mirror of
https://github.com/mihonapp/mihon.git
synced 2025-06-26 19:17:51 +02:00
Compare commits
747 Commits
Author | SHA1 | Date | |
---|---|---|---|
e863e8c64b | |||
f5b591430c | |||
8cfaf8eb51 | |||
675c0cefc3 | |||
1a52385b78 | |||
372e500590 | |||
cc1a317439 | |||
6d650518a1 | |||
7940117577 | |||
b0f87fdd21 | |||
dc92ffed87 | |||
4af578e310 | |||
e22825d818 | |||
e2da6259e7 | |||
d149017c60 | |||
afc400121b | |||
ef993515c6 | |||
edb1d21ddc | |||
ba8abd94a8 | |||
c6d4e4c15f | |||
09f0ac866f | |||
7ed25704d6 | |||
2196dac63e | |||
c8f70efded | |||
ea97488670 | |||
c2255b0a0f | |||
f754b081ce | |||
07771cb5e4 | |||
690d8e43ae | |||
82f14a7d59 | |||
b284384f0a | |||
1ae0d1b5d0 | |||
9de08c8166 | |||
a2d007f2a9 | |||
774f818bbb | |||
0ec7121b8f | |||
d7d46f4447 | |||
45fad147bf | |||
3664195c71 | |||
fce3cd00a1 | |||
33b3be0d0e | |||
cfd1b4a6c6 | |||
d45fefd6f0 | |||
f125ab01ee | |||
be001d090c | |||
971d8a7e40 | |||
a2cf210a52 | |||
3eec207166 | |||
b5d83bdb56 | |||
2c495c4119 | |||
7c72d6cb7c | |||
8362bf0886 | |||
1a8155c45b | |||
3f2f946019 | |||
2c14a8dee1 | |||
917a283bd1 | |||
3e403d5ab3 | |||
746d35b52b | |||
9a7a03e327 | |||
a051079c6a | |||
7b3c18bb97 | |||
52daf3d58c | |||
f41bde5ee1 | |||
6151318ac1 | |||
b45c322729 | |||
b00e8768dc | |||
156feb6e8e | |||
e942b8a402 | |||
abdb67a123 | |||
ee20787c5e | |||
ec4e631760 | |||
02b430a5bf | |||
7878053df2 | |||
12a593c3c6 | |||
6b1f130750 | |||
bde4c0a648 | |||
5ae4621da1 | |||
5ea8d0546e | |||
8a064c118f | |||
2f91c27df2 | |||
763bd54707 | |||
0ea3cc7ce4 | |||
0de3558ab3 | |||
069f4e12d8 | |||
ae4dfc9956 | |||
ee711dc0fb | |||
c316e7faab | |||
7083b3d912 | |||
2d3a1b6a9e | |||
0df23ab878 | |||
7ed8de2ef4 | |||
d935e22f0d | |||
0e26abf7a6 | |||
59aef13200 | |||
9d1f6c4416 | |||
b9f7660a91 | |||
18b5250ed1 | |||
f683f21ee2 | |||
bd033db84c | |||
ab036312a4 | |||
634da15191 | |||
cea1720ea0 | |||
3f2f542265 | |||
b77edb2b5b | |||
1b699bb814 | |||
333c035fed | |||
ce29914c56 | |||
70e5361146 | |||
e7d6dfff53 | |||
eebfad5a95 | |||
77c0a93ac6 | |||
63a3e126b3 | |||
3ea84cf0ce | |||
7fa80ae556 | |||
925f71af15 | |||
c666dd623d | |||
2cd8733212 | |||
4b2a9bc621 | |||
12a9d0575d | |||
edcfa28b0b | |||
3155829994 | |||
d25707554e | |||
38df44ef4b | |||
df683375b1 | |||
cc3cbbc4bb | |||
6922394b8e | |||
24fd82d773 | |||
57aefcd917 | |||
b3854ad382 | |||
5f5fc77877 | |||
0493e77cff | |||
6240fe1dfc | |||
beb7f90908 | |||
a3917972b4 | |||
7094fef37f | |||
0f41e56a24 | |||
52b283283f | |||
ebb15bf96c | |||
6c527d52fb | |||
b8ea57e097 | |||
909aed4262 | |||
4d2fff9538 | |||
9a45983f17 | |||
11926014da | |||
72002c13d6 | |||
6ed767ae84 | |||
3826b307f7 | |||
887b157056 | |||
d36dd39743 | |||
dd008bc13a | |||
50b282f58b | |||
f8a7efbce7 | |||
7d2caeb270 | |||
708e71a35a | |||
4eaccc966e | |||
3670d649b8 | |||
90ab04e81d | |||
26b8df5354 | |||
11a8046c5f | |||
da16110e1c | |||
914b686c8e | |||
27133520fc | |||
24b967ad5c | |||
ca4b4a3f1e | |||
faef35ec47 | |||
326d4c2641 | |||
83436c9550 | |||
2084822731 | |||
071bad1232 | |||
ae1a76da2b | |||
fbc6965c4e | |||
57a5862840 | |||
91fbccdbaa | |||
0ab0dd95ae | |||
bc41040fd3 | |||
4c8dfd0c0c | |||
2b9dbfb390 | |||
84d546b724 | |||
63053b9940 | |||
2256030a2a | |||
79da33b597 | |||
7d67450e58 | |||
8aa11951bf | |||
f23f22ab01 | |||
96a64c7bd2 | |||
d1bb0fdf1d | |||
feca30d7ed | |||
b650151693 | |||
bb3afd0dc9 | |||
5e77ae208d | |||
24e5a4d7ec | |||
1d10d29fa9 | |||
9b00e91773 | |||
cd73c30d6f | |||
7bbba0c7d9 | |||
7907a4fc24 | |||
2f94f62a56 | |||
85791a9336 | |||
a4eba50cfd | |||
03980b2f27 | |||
664e5cfb59 | |||
b9736df7e0 | |||
f48b2681e3 | |||
ab46bd56b0 | |||
c23506e887 | |||
9ad67a7b7d | |||
7a1b6142df | |||
478256d766 | |||
4d92caacef | |||
fd45de5c58 | |||
bcaa9674fe | |||
40aa3b7e18 | |||
5aea21a194 | |||
b5e118e2b4 | |||
dfec0e45ed | |||
ff2a4e6952 | |||
7660751f7f | |||
78b9ac4766 | |||
d5c75571dc | |||
16b9c459ab | |||
41c060e28b | |||
a3090e62f5 | |||
39b7024be0 | |||
d019c5999b | |||
20264eecb9 | |||
cc55453076 | |||
6cab2427f5 | |||
511bcc9197 | |||
00ac632d8f | |||
649209890d | |||
f2fca0f13d | |||
4084d5e69a | |||
e8beb7103c | |||
0e4ce0f1ae | |||
c42d517f6b | |||
356cd4ef52 | |||
88619145d8 | |||
6ba779fb7a | |||
8bd965267c | |||
7f76ffa5cb | |||
4acc7cee3d | |||
be28e0b559 | |||
116fec208b | |||
fece92e15a | |||
dce3049446 | |||
fcd6fe5d8a | |||
a69a833716 | |||
697b082591 | |||
b2d58e04d2 | |||
8bfc5f0450 | |||
a252a8acee | |||
447ee4bd09 | |||
3cd6382795 | |||
5d1134dfa8 | |||
05e7b0dc22 | |||
c0647c3110 | |||
ef84ed4982 | |||
a1e83b9f19 | |||
4ce4ee3c00 | |||
0d62aedfbb | |||
b7c2890250 | |||
ae97bb0445 | |||
117fd4bd0f | |||
bd424ce460 | |||
1dddba7f25 | |||
7fd75b7501 | |||
423f07033e | |||
ef9c457681 | |||
a6d4a3b785 | |||
2e487f8a3f | |||
2423a70abd | |||
13d39fc942 | |||
b7547a8458 | |||
8931dbb657 | |||
52416ff3a8 | |||
3dbfee91f6 | |||
09d4901781 | |||
62955e7385 | |||
1ef7722504 | |||
24bb2f02dc | |||
627698d81f | |||
d4c8480dee | |||
015e8deb79 | |||
714aa4b4ba | |||
8d5f798591 | |||
e65f59b3df | |||
341c3d179e | |||
67128937ca | |||
d9ea621e54 | |||
fb35d7af59 | |||
c254aa6fcc | |||
37d30eb887 | |||
49cdcc644c | |||
07e5525c74 | |||
776194f5b2 | |||
ed80ee98a7 | |||
040bac3da2 | |||
9df721d158 | |||
c50ede8b2c | |||
ba0907ae59 | |||
e9dce32a98 | |||
535cc0d81e | |||
5801297d78 | |||
51a33a47cd | |||
01a1a9ebab | |||
438bad9649 | |||
fe3b36caeb | |||
83588e14d9 | |||
64b1c9636b | |||
db0c1b2634 | |||
568c4d8c8e | |||
d645507eeb | |||
3548112ab2 | |||
0cb042cd93 | |||
0eadc028b6 | |||
82f3677168 | |||
70ed49e478 | |||
3c67a36b60 | |||
e5621246ec | |||
cb71d44024 | |||
7e3ea9074c | |||
e2cf157857 | |||
60890147c3 | |||
64c95305b9 | |||
feddd9285d | |||
d1b393965f | |||
e31a39b9d5 | |||
98fc028d39 | |||
88fd799a30 | |||
ef937f277e | |||
c3fb5af3fc | |||
859e8deb02 | |||
932c92412c | |||
05771ddf6d | |||
848d387ec4 | |||
ac6b4235b9 | |||
ab73e98075 | |||
aecdd04e04 | |||
e5cdf74587 | |||
8d25ce7323 | |||
8deca3b63a | |||
9b967177c5 | |||
4dfb3cc972 | |||
73e5e9ecd9 | |||
653b7ffcd0 | |||
8791b72cb1 | |||
d961492380 | |||
07de367476 | |||
31d96c2bf0 | |||
fb8aafb69f | |||
3d58b78062 | |||
ec5e6958ef | |||
71bd5fe367 | |||
6385c71c72 | |||
d43255e688 | |||
3527dedc99 | |||
de50f53be4 | |||
f2e4b2fc99 | |||
e6f3cd03bb | |||
a1e31549a2 | |||
71d225c562 | |||
7c23212850 | |||
fdf178d4df | |||
04ebca8413 | |||
edeee54fb2 | |||
a906e9b302 | |||
fff72b61df | |||
74381ef59e | |||
64f95af3e5 | |||
85a1eb75c9 | |||
597cec3064 | |||
b03ebc1fa4 | |||
6c53bb4d51 | |||
fb7a458747 | |||
db25a9ae4f | |||
c69420373a | |||
2b8347f899 | |||
281a3911f6 | |||
9b77dd9a2b | |||
cb8cff3179 | |||
3db85c7274 | |||
b41ac355a0 | |||
88d9ffe92e | |||
5113c78ab6 | |||
3854995ef2 | |||
36e14b951a | |||
9299a4beff | |||
d681bea395 | |||
0f3f1e9226 | |||
79ab492a5b | |||
62db4bb09d | |||
7be2cbb75b | |||
5b1fe3460f | |||
31997fe50a | |||
5e5ceef122 | |||
40edbac7f0 | |||
5bb1f72c28 | |||
8622e6492c | |||
1feac9c559 | |||
fce81dd6d9 | |||
aa50554f06 | |||
034506f56b | |||
2d8858edb4 | |||
b2601ad696 | |||
8099f561c5 | |||
8a014ddb0c | |||
3d9383ce67 | |||
9de07c11a6 | |||
9f744bc445 | |||
aed6e12119 | |||
c57d0046bc | |||
07b9fc9b31 | |||
2c6bcb85a0 | |||
fefa519486 | |||
11a232a2df | |||
8dcd919ff0 | |||
d9c27e7109 | |||
8af8c57bb4 | |||
a1a4916abf | |||
9be8f675ac | |||
a271c3726e | |||
8c18a14dfd | |||
9a801cfdfb | |||
4af13e3536 | |||
e76e903060 | |||
3d89a317c1 | |||
d8251224cb | |||
acd927a937 | |||
a498f940c6 | |||
948cb31d1a | |||
179cb8eb50 | |||
47f865aa72 | |||
b47face2f8 | |||
69869115f6 | |||
0fb9ca3e8b | |||
eaf9c9b2d8 | |||
70d9b0c390 | |||
e57a999c9c | |||
3b49289cfb | |||
176e984b56 | |||
b5a700276a | |||
3c186a3c8d | |||
a462ce3626 | |||
065cf42aea | |||
986b709f2c | |||
fed6f44995 | |||
1b52acdad7 | |||
10a638c6b8 | |||
7875f363a8 | |||
685736b9ec | |||
aefd2bf6f8 | |||
ce9fb2f1fe | |||
974275a429 | |||
98461f9bca | |||
094f78fb41 | |||
33dcdc1599 | |||
8870ccb18c | |||
2a7ed1375a | |||
107727eea9 | |||
54b50cca71 | |||
1c10ba7925 | |||
2b8df691ff | |||
15da856303 | |||
cef5343a24 | |||
f96b85fcb2 | |||
a62628423f | |||
ef8a87a30f | |||
89fb943733 | |||
147978b932 | |||
c741920ec0 | |||
bbbcb18b91 | |||
d6b3b0baf7 | |||
dbe8931cf0 | |||
d2eb5d7f45 | |||
562dce60ee | |||
569df39fb8 | |||
2f7f00c7a2 | |||
afd59eabbb | |||
cf99446a12 | |||
68286b2acc | |||
a410184e0a | |||
d3ceecf620 | |||
940c5b3838 | |||
17c321286d | |||
0dbb79359b | |||
19f39fcdb0 | |||
ab021c1302 | |||
3b11ad8de8 | |||
cf4b870846 | |||
5e37f72d74 | |||
6843dbf7e1 | |||
09c07faafd | |||
8e7c235ff0 | |||
7fb4cbb8a0 | |||
fa872f6cf7 | |||
ef53d4ec07 | |||
c68e7c8da7 | |||
de35a4c62a | |||
fcde6c2b84 | |||
9cbe053e79 | |||
818468c58f | |||
7ba43ae5c2 | |||
5700c7a0c7 | |||
4bfd395d9f | |||
5069d8dee6 | |||
47c120e58c | |||
8d7ab13f5c | |||
122cdae5bc | |||
157d8db68c | |||
998da965cd | |||
8d58a8d548 | |||
b453be081e | |||
3c947f323f | |||
cb203ef02c | |||
908c9bc624 | |||
fe373a95a2 | |||
60f18f3b5a | |||
284c019b32 | |||
32434471e5 | |||
6a4c280235 | |||
f0eacf4218 | |||
0afe3011bc | |||
0fef546a0d | |||
93e6136795 | |||
7d23fd8ef5 | |||
71c9df5279 | |||
224fcada17 | |||
9278407b85 | |||
dad3292bdd | |||
cfdf319972 | |||
89619b7836 | |||
6aff438a16 | |||
13324dd1a1 | |||
ae9bf06b46 | |||
5236834911 | |||
bf80dd622c | |||
662b71436e | |||
f608cb55eb | |||
6ba82da029 | |||
f407e30b6e | |||
4e7b8c98f9 | |||
5f9574541f | |||
08a6db7d6e | |||
b485e1d657 | |||
e8d8621f06 | |||
4cefbce7c3 | |||
fa31369f99 | |||
d0bf93ebb7 | |||
41a747c7e7 | |||
8882cd4787 | |||
6676490e09 | |||
68bea8a196 | |||
25995c09a0 | |||
0eb8d7d081 | |||
554f890ae3 | |||
dd1743698f | |||
b092e98ac9 | |||
9ee6262aed | |||
24a2d86f41 | |||
b5c5c66336 | |||
7654feb6a8 | |||
a598ac3993 | |||
cab919d74c | |||
60a929b92c | |||
356b7c346a | |||
ad57fde1c5 | |||
17f7dea21b | |||
b40af7c3c6 | |||
9065362fde | |||
d264b03ca1 | |||
ad9bad3d17 | |||
dfd858034f | |||
58ad8fa8c0 | |||
38610d8a24 | |||
27cec697bf | |||
024f9a8c76 | |||
f7cc36f2f0 | |||
ef5148ebb4 | |||
6dbc0a6fd5 | |||
fba3f9d501 | |||
d9f8137362 | |||
28416489b2 | |||
54a23ddd1f | |||
3287ca9cf2 | |||
a59e134862 | |||
1f8c5b0120 | |||
c7f839ea4a | |||
d981245723 | |||
1f729f1cb3 | |||
b4577d6676 | |||
544adb9940 | |||
1875c4a752 | |||
5f0493f1e5 | |||
c749e50bec | |||
a4e5e3ece5 | |||
2a69d1b051 | |||
126e1e2d9d | |||
0586e1d3ad | |||
07cb1c237e | |||
f4f1efe5fa | |||
37fdf4d434 | |||
99b46096a4 | |||
12e90ae35e | |||
023311a874 | |||
155a4dd463 | |||
15bed1ac4c | |||
27f55f8098 | |||
00598879e2 | |||
df274a0a78 | |||
0dc4862d79 | |||
a3f1b72126 | |||
5ff10799e4 | |||
a82e5f5452 | |||
e10cb0e632 | |||
c7e07a6df0 | |||
2e0c778090 | |||
592050c668 | |||
02c9191525 | |||
d421401626 | |||
b2d4e5ab84 | |||
84e023607c | |||
f145fd0dec | |||
42a9f911d8 | |||
9567d55312 | |||
531cd99247 | |||
f3660d88dd | |||
3accb9a08b | |||
63ce7371bb | |||
01c3498dbf | |||
b3471234ad | |||
b2d697131c | |||
ef49fc91d8 | |||
6222b47a4f | |||
f58e3c390a | |||
7504621a24 | |||
88e49a9b8b | |||
5b23f29d06 | |||
c1bdebee78 | |||
ddd4cc10ff | |||
0ca62a4acc | |||
4f1275ac01 | |||
b2fee7035f | |||
e15d7cb548 | |||
3257cbe21f | |||
1237af1ff3 | |||
68600b337e | |||
dac2072eaa | |||
1b921f9845 | |||
a3992d9fbe | |||
efd2a0cb7b | |||
fba428257b | |||
ff36901007 | |||
940d8389b5 | |||
f7a6cbe5e2 | |||
7aa379a857 | |||
443024cebb | |||
1657f04d55 | |||
407e798fdb | |||
4054f2a6a0 | |||
468cdf603c | |||
988ec6a224 | |||
bdbdf211e2 | |||
0437703cbf | |||
71aa592111 | |||
d501c02f8b | |||
9daf0e78b8 | |||
dfa07a5f35 | |||
437c995d12 | |||
cc6ae9d1a8 | |||
c58e4f4dee | |||
c87b0e77de | |||
355d5af8ae | |||
3d99a8ebdb | |||
c4b975b777 | |||
2911fe7a1a | |||
14c114756d | |||
e7a8107279 | |||
bff73b1b40 | |||
c255f57d95 | |||
64c47bbaed | |||
e0b7698d40 | |||
a01792ac9a | |||
3ba078f64c | |||
a16240f123 | |||
e5a120e778 | |||
2ba60e9114 | |||
472ce5a5e4 | |||
99ba84c810 | |||
78285bdf37 | |||
5a7f2684b3 | |||
d912a42249 | |||
6d8c4fb8b1 | |||
a63cecbfcb | |||
4a5bceb4e4 | |||
86541445b7 | |||
4e826aa8e7 | |||
b6e6f490e9 | |||
2145e878a4 | |||
355f6db255 | |||
bc7632bf02 | |||
609d8c9685 | |||
2f08515455 | |||
7f450e185d | |||
747879b4ec | |||
4193870fa6 | |||
cdc5de3f1b | |||
bc34d4fa88 | |||
6fd4af8736 | |||
b5c2934270 | |||
94f5117941 | |||
112e233498 | |||
18b1326f3a | |||
1e58b05ead | |||
938919bd9b | |||
b6b78994d8 | |||
fddd8ce305 | |||
ccff337975 | |||
fde6b7af4f | |||
0657db7dcb | |||
d1c2eaf6d5 | |||
91bb6b9016 | |||
90351c6e9e | |||
dd4740e54f | |||
48e7cbd76c | |||
f51e32f39b | |||
ae42f59102 | |||
5c8006f9b7 | |||
aa5861d3ca | |||
7a64bf55cb | |||
d4c9ab793f | |||
48d2849d97 | |||
776610d0e6 | |||
3a790f3d66 | |||
7382042288 | |||
33992d80bf | |||
a92b0e567b | |||
829a65e515 | |||
03ad48c055 | |||
89837e4ced | |||
ace1db21d1 | |||
8bb69c455b | |||
2dae706198 | |||
3eda2a220a | |||
61e5440b7c | |||
2e2663bad9 | |||
f4dd150b70 | |||
2b35d22e25 |
1
.github/FUNDING.yml
vendored
1
.github/FUNDING.yml
vendored
@ -1,2 +1 @@
|
|||||||
github: inorichi
|
|
||||||
ko_fi: inorichi
|
ko_fi: inorichi
|
||||||
|
12
.github/ISSUE_TEMPLATE.md
vendored
12
.github/ISSUE_TEMPLATE.md
vendored
@ -2,9 +2,15 @@
|
|||||||
|
|
||||||
I acknowledge that:
|
I acknowledge that:
|
||||||
|
|
||||||
- I have updated to the latest version of the app (stable is v0.10.9)
|
- I have updated:
|
||||||
- I have updated all extensions
|
- To the latest version of the app (stable is v0.12.3)
|
||||||
|
- All extensions
|
||||||
|
- I have tried the troubleshooting guide: https://tachiyomi.org/help/guides/troubleshooting-problems/
|
||||||
- If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions
|
- If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions
|
||||||
|
- I have searched the existing issues and this is new ticket **NOT** a duplicate or related to another open issue
|
||||||
|
- I will fill out the title and the information in this template
|
||||||
|
|
||||||
|
Note that the issue will be automatically closed if you do not fill out the title or requested information.
|
||||||
|
|
||||||
**DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT**
|
**DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT**
|
||||||
|
|
||||||
@ -24,3 +30,5 @@ I acknowledge that:
|
|||||||
|
|
||||||
## Other details
|
## Other details
|
||||||
Additional details and attachments.
|
Additional details and attachments.
|
||||||
|
|
||||||
|
If you're experiencing crashes, share the crash logs from More → Settings → Advanced → Dump crash logs.
|
||||||
|
36
.github/ISSUE_TEMPLATE/bug_report.md
vendored
36
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -1,36 +0,0 @@
|
|||||||
---
|
|
||||||
name: "🐞 Bug report"
|
|
||||||
about: Report a bug
|
|
||||||
title: "[Bug] <Write short description here>"
|
|
||||||
labels: "bug"
|
|
||||||
---
|
|
||||||
|
|
||||||
**PLEASE READ THIS**
|
|
||||||
|
|
||||||
I acknowledge that:
|
|
||||||
|
|
||||||
- I have updated to the latest version of the app (stable is v0.10.9)
|
|
||||||
- I have updated all extensions
|
|
||||||
- If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions
|
|
||||||
|
|
||||||
**DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Device information
|
|
||||||
* Tachiyomi version: ?
|
|
||||||
* Android version: ?
|
|
||||||
* Device: ?
|
|
||||||
|
|
||||||
## Steps to reproduce
|
|
||||||
1. First step
|
|
||||||
2. Second step
|
|
||||||
|
|
||||||
### Expected behavior
|
|
||||||
This should happen.
|
|
||||||
|
|
||||||
### Actual behavior
|
|
||||||
This happened instead.
|
|
||||||
|
|
||||||
## Other details
|
|
||||||
Additional details and attachments.
|
|
13
.github/ISSUE_TEMPLATE/config.yml
vendored
13
.github/ISSUE_TEMPLATE/config.yml
vendored
@ -1,8 +1,11 @@
|
|||||||
blank_issues_enabled: false
|
blank_issues_enabled: false
|
||||||
contact_links:
|
contact_links:
|
||||||
- name: Tachiyomi help website
|
- name: ⚠️ Extension/source issue
|
||||||
|
url: https://github.com/tachiyomiorg/tachiyomi-extensions/issues/new/choose
|
||||||
|
about: Issues and requests for extensions and sources should be opened in the tachiyomi-extensions repository instead
|
||||||
|
- name: 📦 Tachiyomi extensions
|
||||||
|
url: https://tachiyomi.org/extensions
|
||||||
|
about: List of all available extensions with download links
|
||||||
|
- name: 🖥️ Tachiyomi website
|
||||||
url: https://tachiyomi.org/help/
|
url: https://tachiyomi.org/help/
|
||||||
about: Common questions are answered here.
|
about: Guides, troubleshooting, and answers to common questions
|
||||||
- name: Tachiyomi extensions GitHub repository
|
|
||||||
url: https://github.com/tachiyomiorg/tachiyomi-extensions
|
|
||||||
about: Issues about an extension/source/catalogue should be opened here instead.
|
|
||||||
|
24
.github/ISSUE_TEMPLATE/feature_request.md
vendored
24
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@ -1,24 +0,0 @@
|
|||||||
---
|
|
||||||
name: "🌟 Feature request"
|
|
||||||
about: Suggest a feature to improve Tachiyomi
|
|
||||||
title: "[Feature Request] <Write short description here>"
|
|
||||||
labels: "feature"
|
|
||||||
---
|
|
||||||
|
|
||||||
**PLEASE READ THIS**
|
|
||||||
|
|
||||||
I acknowledge that:
|
|
||||||
|
|
||||||
- I have updated to the latest version of the app (stable is v0.10.9)
|
|
||||||
- I have updated all extensions
|
|
||||||
- If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions
|
|
||||||
|
|
||||||
**DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Why/User Benefit/User Problem
|
|
||||||
(explain why this feature should be added)
|
|
||||||
|
|
||||||
## What/Requirements
|
|
||||||
(explain how this feature would behave)
|
|
106
.github/ISSUE_TEMPLATE/report_issue.yml
vendored
Normal file
106
.github/ISSUE_TEMPLATE/report_issue.yml
vendored
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
name: 🐞 Issue report
|
||||||
|
description: Report an issue in Tachiyomi
|
||||||
|
labels: [Bug]
|
||||||
|
body:
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: reproduce-steps
|
||||||
|
attributes:
|
||||||
|
label: Steps to reproduce
|
||||||
|
description: Provide an example of the issue.
|
||||||
|
placeholder: |
|
||||||
|
Example:
|
||||||
|
1. First step
|
||||||
|
2. Second step
|
||||||
|
3. Issue here
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: expected-behavior
|
||||||
|
attributes:
|
||||||
|
label: Expected behavior
|
||||||
|
description: Explain what you should expect to happen.
|
||||||
|
placeholder: |
|
||||||
|
Example:
|
||||||
|
"This should happen..."
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: actual-behavior
|
||||||
|
attributes:
|
||||||
|
label: Actual behavior
|
||||||
|
description: Explain what actually happens.
|
||||||
|
placeholder: |
|
||||||
|
Example:
|
||||||
|
"This happened instead..."
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: crash-logs
|
||||||
|
attributes:
|
||||||
|
label: Crash logs
|
||||||
|
description: |
|
||||||
|
If you're experiencing crashes, share the crash logs from **More → Settings → Advanced** then press **Dump crash logs**.
|
||||||
|
placeholder: |
|
||||||
|
You can paste the crash logs in pure text or upload it as an attachment.
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: tachiyomi-version
|
||||||
|
attributes:
|
||||||
|
label: Tachiyomi version
|
||||||
|
description: You can find your Tachiyomi version in **More → About**.
|
||||||
|
placeholder: |
|
||||||
|
Example: "0.12.3"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: android-version
|
||||||
|
attributes:
|
||||||
|
label: Android version
|
||||||
|
description: You can find this somewhere in your Android settings.
|
||||||
|
placeholder: |
|
||||||
|
Example: "Android 11"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: device
|
||||||
|
attributes:
|
||||||
|
label: Device
|
||||||
|
description: List your device and model.
|
||||||
|
placeholder: |
|
||||||
|
Example: "Google Pixel 5"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: other-details
|
||||||
|
attributes:
|
||||||
|
label: Other details
|
||||||
|
placeholder: |
|
||||||
|
Additional details and attachments.
|
||||||
|
|
||||||
|
- type: checkboxes
|
||||||
|
id: acknowledgements
|
||||||
|
attributes:
|
||||||
|
label: Acknowledgements
|
||||||
|
description: Read this carefully, we will close and ignore your issue if you skimmed through this.
|
||||||
|
options:
|
||||||
|
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open issue.
|
||||||
|
required: true
|
||||||
|
- label: I have written a short but informative title.
|
||||||
|
required: true
|
||||||
|
- label: If this is an issue with an extension, I should be opening an issue in the [extensions repository](https://github.com/tachiyomiorg/tachiyomi-extensions/issues/new/choose).
|
||||||
|
required: true
|
||||||
|
- label: I have tried the [troubleshooting guide](https://tachiyomi.org/help/guides/troubleshooting/).
|
||||||
|
required: true
|
||||||
|
- label: I have updated the app to version **[0.12.3](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**.
|
||||||
|
required: true
|
||||||
|
- label: I have updated all installed extensions.
|
||||||
|
required: true
|
||||||
|
- label: I will fill out all of the requested information in this form.
|
||||||
|
required: true
|
39
.github/ISSUE_TEMPLATE/request_feature.yml
vendored
Normal file
39
.github/ISSUE_TEMPLATE/request_feature.yml
vendored
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
name: ⭐ Feature request
|
||||||
|
description: Suggest a feature to improve Tachiyomi
|
||||||
|
labels: [Feature request]
|
||||||
|
body:
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: feature-description
|
||||||
|
attributes:
|
||||||
|
label: Describe your suggested feature
|
||||||
|
description: How can Tachiyomi be improved?
|
||||||
|
placeholder: |
|
||||||
|
Example:
|
||||||
|
"It should work like this..."
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: other-details
|
||||||
|
attributes:
|
||||||
|
label: Other details
|
||||||
|
placeholder: |
|
||||||
|
Additional details and attachments.
|
||||||
|
|
||||||
|
- type: checkboxes
|
||||||
|
id: acknowledgements
|
||||||
|
attributes:
|
||||||
|
label: Acknowledgements
|
||||||
|
description: Read this carefully, we will close and ignore your issue if you skimmed through this.
|
||||||
|
options:
|
||||||
|
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open issue.
|
||||||
|
required: true
|
||||||
|
- label: I have written a short but informative title.
|
||||||
|
required: true
|
||||||
|
- label: If this is an issue with an extension, I should be opening an issue in the [extensions repository](https://github.com/tachiyomiorg/tachiyomi-extensions/issues/new/choose).
|
||||||
|
required: true
|
||||||
|
- label: I have updated the app to version **[0.12.3](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**.
|
||||||
|
required: true
|
||||||
|
- label: I will fill out all of the requested information in this form.
|
||||||
|
required: true
|
8
.github/ISSUE_TEMPLATE/source_issue.md
vendored
8
.github/ISSUE_TEMPLATE/source_issue.md
vendored
@ -1,8 +0,0 @@
|
|||||||
---
|
|
||||||
name: "Extension/source/catalogue issue"
|
|
||||||
about: "Do not open an issue here. See https://github.com/tachiyomiorg/tachiyomi-extensions"
|
|
||||||
title: "THIS ISSUE IS IN THE WRONG REPO; SEE https://github.com/tachiyomiorg/tachiyomi-extensions"
|
|
||||||
labels: "catalog, invalid"
|
|
||||||
---
|
|
||||||
|
|
||||||
DO NOT OPEN AN ISSUE IN THIS REPO. SEE https://github.com/tachiyomiorg/tachiyomi-extensions
|
|
12
.github/pull_request_template.md
vendored
Normal file
12
.github/pull_request_template.md
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<!--
|
||||||
|
Please include a summary of the change and which issue is fixed.
|
||||||
|
Also make sure you've tested your code and also done a self-review of it.
|
||||||
|
Don't forget to check all base themes and tablet mode for relevant changes.
|
||||||
|
|
||||||
|
If your changes are visual, please provide images below:
|
||||||
|
|
||||||
|
### Images
|
||||||
|
| Image 1 | Image 2 |
|
||||||
|
| ------- | ------- |
|
||||||
|
|  |  |
|
||||||
|
-->
|
BIN
.github/readme-images/screens.png
vendored
BIN
.github/readme-images/screens.png
vendored
Binary file not shown.
Before Width: | Height: | Size: 1.2 MiB |
95
.github/workflows/build.yml
vendored
95
.github/workflows/build.yml
vendored
@ -1,95 +0,0 @@
|
|||||||
name: CI
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
tags:
|
|
||||||
- v*
|
|
||||||
pull_request:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
check_wrapper:
|
|
||||||
name: Validate Gradle Wrapper
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Clone repo
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
|
|
||||||
- name: Validate Gradle Wrapper
|
|
||||||
uses: gradle/wrapper-validation-action@v1
|
|
||||||
|
|
||||||
build:
|
|
||||||
name: Build app
|
|
||||||
needs: check_wrapper
|
|
||||||
if: "!startsWith(github.event.head_commit.message, '[SKIP CI]')"
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Cancel previous runs
|
|
||||||
uses: styfle/cancel-workflow-action@0.5.0
|
|
||||||
with:
|
|
||||||
access_token: ${{ github.token }}
|
|
||||||
|
|
||||||
- name: Clone repo
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
|
|
||||||
- name: Set up JDK 11
|
|
||||||
uses: actions/setup-java@v1
|
|
||||||
with:
|
|
||||||
java-version: 11
|
|
||||||
|
|
||||||
- name: Copy CI gradle.properties
|
|
||||||
run: |
|
|
||||||
mkdir -p ~/.gradle
|
|
||||||
cp .github/runner-files/ci-gradle.properties ~/.gradle/gradle.properties
|
|
||||||
|
|
||||||
- name: Build app
|
|
||||||
uses: eskatos/gradle-command-action@v1
|
|
||||||
with:
|
|
||||||
arguments: assembleStandardRelease
|
|
||||||
wrapper-cache-enabled: true
|
|
||||||
dependencies-cache-enabled: true
|
|
||||||
configuration-cache-enabled: true
|
|
||||||
|
|
||||||
# Sign APK and create release for tags
|
|
||||||
|
|
||||||
- name: Get tag name
|
|
||||||
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'tachiyomiorg/tachiyomi'
|
|
||||||
id: get_tag_name
|
|
||||||
run: |
|
|
||||||
set -x
|
|
||||||
echo "VERSION_TAG=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Sign APK
|
|
||||||
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'tachiyomiorg/tachiyomi'
|
|
||||||
uses: r0adkll/sign-android-release@v1
|
|
||||||
with:
|
|
||||||
releaseDirectory: app/build/outputs/apk/standard/release
|
|
||||||
signingKeyBase64: ${{ secrets.SIGNING_KEY }}
|
|
||||||
alias: ${{ secrets.ALIAS }}
|
|
||||||
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
|
|
||||||
keyPassword: ${{ secrets.KEY_PASSWORD }}
|
|
||||||
|
|
||||||
- name: Create release
|
|
||||||
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'tachiyomiorg/tachiyomi'
|
|
||||||
id: create_release
|
|
||||||
uses: actions/create-release@v1
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
with:
|
|
||||||
tag_name: ${{ env.VERSION_TAG }}
|
|
||||||
release_name: Tachiyomi ${{ env.VERSION_TAG }}
|
|
||||||
draft: true
|
|
||||||
prerelease: false
|
|
||||||
|
|
||||||
- name: Upload APK to release
|
|
||||||
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'tachiyomiorg/tachiyomi'
|
|
||||||
uses: actions/upload-release-asset@v1
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
with:
|
|
||||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
|
||||||
asset_path: ${{ env.SIGNED_RELEASE_FILE }}
|
|
||||||
asset_name: tachiyomi-${{ env.VERSION_TAG }}.apk
|
|
||||||
asset_content_type: application/vnd.android.package-archive
|
|
33
.github/workflows/build_pull_request.yml
vendored
Normal file
33
.github/workflows/build_pull_request.yml
vendored
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
name: PR build check
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build app
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Clone repo
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Validate Gradle Wrapper
|
||||||
|
uses: gradle/wrapper-validation-action@v1
|
||||||
|
|
||||||
|
- name: Set up JDK 11
|
||||||
|
uses: actions/setup-java@v1
|
||||||
|
with:
|
||||||
|
java-version: 11
|
||||||
|
|
||||||
|
- name: Copy CI gradle.properties
|
||||||
|
run: |
|
||||||
|
mkdir -p ~/.gradle
|
||||||
|
cp .github/runner-files/ci-gradle.properties ~/.gradle/gradle.properties
|
||||||
|
|
||||||
|
- name: Build app
|
||||||
|
uses: gradle/gradle-command-action@v1
|
||||||
|
with:
|
||||||
|
arguments: assembleStandardRelease
|
||||||
|
distributions-cache-enabled: true
|
||||||
|
dependencies-cache-enabled: true
|
||||||
|
configuration-cache-enabled: true
|
108
.github/workflows/build_push.yml
vendored
Normal file
108
.github/workflows/build_push.yml
vendored
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
name: CI
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
tags:
|
||||||
|
- v*
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build app
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Cancel previous runs
|
||||||
|
uses: styfle/cancel-workflow-action@0.5.0
|
||||||
|
with:
|
||||||
|
access_token: ${{ github.token }}
|
||||||
|
|
||||||
|
- name: Clone repo
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Validate Gradle Wrapper
|
||||||
|
uses: gradle/wrapper-validation-action@v1
|
||||||
|
|
||||||
|
- name: Set up JDK 11
|
||||||
|
uses: actions/setup-java@v1
|
||||||
|
with:
|
||||||
|
java-version: 11
|
||||||
|
|
||||||
|
- name: Copy CI gradle.properties
|
||||||
|
run: |
|
||||||
|
mkdir -p ~/.gradle
|
||||||
|
cp .github/runner-files/ci-gradle.properties ~/.gradle/gradle.properties
|
||||||
|
|
||||||
|
- name: Build app
|
||||||
|
uses: gradle/gradle-command-action@v1
|
||||||
|
with:
|
||||||
|
arguments: assembleStandardRelease
|
||||||
|
distributions-cache-enabled: true
|
||||||
|
dependencies-cache-enabled: true
|
||||||
|
configuration-cache-enabled: true
|
||||||
|
|
||||||
|
# Sign APK and create release for tags
|
||||||
|
|
||||||
|
- name: Get tag name
|
||||||
|
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'tachiyomiorg/tachiyomi'
|
||||||
|
run: |
|
||||||
|
set -x
|
||||||
|
echo "VERSION_TAG=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Sign APK
|
||||||
|
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'tachiyomiorg/tachiyomi'
|
||||||
|
uses: r0adkll/sign-android-release@v1
|
||||||
|
with:
|
||||||
|
releaseDirectory: app/build/outputs/apk/standard/release
|
||||||
|
signingKeyBase64: ${{ secrets.SIGNING_KEY }}
|
||||||
|
alias: ${{ secrets.ALIAS }}
|
||||||
|
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
|
||||||
|
keyPassword: ${{ secrets.KEY_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Clean up build artifacts
|
||||||
|
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'tachiyomiorg/tachiyomi'
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
|
||||||
|
mv app/build/outputs/apk/standard/release/app-standard-universal-release-unsigned-signed.apk tachiyomi-${{ env.VERSION_TAG }}.apk
|
||||||
|
sha=`sha256sum tachiyomi-${{ env.VERSION_TAG }}.apk | awk '{ print $1 }'`
|
||||||
|
echo "APK_UNIVERSAL_SHA=$sha" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
cp app/build/outputs/apk/standard/release/app-standard-arm64-v8a-release-unsigned-signed.apk tachiyomi-arm64-v8a-${{ env.VERSION_TAG }}.apk
|
||||||
|
sha=`sha256sum tachiyomi-arm64-v8a-${{ env.VERSION_TAG }}.apk | awk '{ print $1 }'`
|
||||||
|
echo "APK_ARM64_V8A_SHA=$sha" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
cp app/build/outputs/apk/standard/release/app-standard-armeabi-v7a-release-unsigned-signed.apk tachiyomi-armeabi-v7a-${{ env.VERSION_TAG }}.apk
|
||||||
|
sha=`sha256sum tachiyomi-armeabi-v7a-${{ env.VERSION_TAG }}.apk | awk '{ print $1 }'`
|
||||||
|
echo "APK_ARMEABI_V7A_SHA=$sha" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
cp app/build/outputs/apk/standard/release/app-standard-x86-release-unsigned-signed.apk tachiyomi-x86-${{ env.VERSION_TAG }}.apk
|
||||||
|
sha=`sha256sum tachiyomi-x86-${{ env.VERSION_TAG }}.apk | awk '{ print $1 }'`
|
||||||
|
echo "APK_X86_SHA=$sha" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Create Release
|
||||||
|
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'tachiyomiorg/tachiyomi'
|
||||||
|
uses: softprops/action-gh-release@v1
|
||||||
|
with:
|
||||||
|
tag_name: ${{ env.VERSION_TAG }}
|
||||||
|
name: Tachiyomi ${{ env.VERSION_TAG }}
|
||||||
|
body: |
|
||||||
|
---
|
||||||
|
|
||||||
|
### Checksums
|
||||||
|
|
||||||
|
| Variant | SHA-256 |
|
||||||
|
| ------- | ------- |
|
||||||
|
| Universal | ${{ env.APK_UNIVERSAL_SHA }}
|
||||||
|
| arm64-v8a | ${{ env.APK_ARM64_V8A_SHA }}
|
||||||
|
| armeabi-v7a | ${{ env.APK_ARMEABI_V7A_SHA }}
|
||||||
|
| x86 | ${{ env.APK_X86_SHA }} |
|
||||||
|
files: |
|
||||||
|
tachiyomi-${{ env.VERSION_TAG }}.apk
|
||||||
|
tachiyomi-arm64-v8a-${{ env.VERSION_TAG }}.apk
|
||||||
|
tachiyomi-armeabi-v7a-${{ env.VERSION_TAG }}.apk
|
||||||
|
tachiyomi-x86-${{ env.VERSION_TAG }}.apk
|
||||||
|
draft: true
|
||||||
|
prerelease: false
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
15
.github/workflows/cancel_pull_request.yml
vendored
Normal file
15
.github/workflows/cancel_pull_request.yml
vendored
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
name: Cancel old pull request workflows
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_run:
|
||||||
|
workflows: ["PR build check"]
|
||||||
|
types:
|
||||||
|
- requested
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
cancel:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: styfle/cancel-workflow-action@0.8.0
|
||||||
|
with:
|
||||||
|
workflow_id: ${{ github.event.workflow.id }}
|
51
.github/workflows/issue_closer.yml
vendored
51
.github/workflows/issue_closer.yml
vendored
@ -7,31 +7,26 @@ jobs:
|
|||||||
autoclose:
|
autoclose:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Autoclose when created in wrong repo
|
- name: Autoclose issues
|
||||||
uses: arkon/issue-closer-action@v1.1
|
uses: arkon/issue-closer-action@v3.4
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
type: title
|
rules: |
|
||||||
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
|
"type": "body",
|
||||||
uses: arkon/issue-closer-action@v1.1
|
"regex": ".*DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT.*",
|
||||||
with:
|
"message": "The acknowledgment section was not removed."
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
},
|
||||||
type: title
|
{
|
||||||
regex: ".*<Write short description here>*"
|
"type": "body",
|
||||||
message: "@${issue.user.login} this issue was automatically closed because you did not fill out the description in the title."
|
"regex": ".*\\* (Tachiyomi version|Android version|Device): \\?.*",
|
||||||
- name: Autoclose when body acknowledgement section not removed
|
"message": "Requested information in the template was not filled out."
|
||||||
uses: arkon/issue-closer-action@v1.1
|
},
|
||||||
with:
|
{
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
"type": "both",
|
||||||
type: body
|
"regex": "^(?!.*myanimelist.*).*(aniyomi|anime).*$",
|
||||||
regex: ".*DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT.*"
|
"ignoreCase": true,
|
||||||
message: "@${issue.user.login} this issue was automatically closed because the acknowledgment section was not removed."
|
"message": "Tachiyomi does not support anime, and has no plans to support anime. In addition Tachiyomi is not affiliated with Aniyomi https://github.com/jmir1/aniyomi"
|
||||||
- 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."
|
|
||||||
|
14
.github/workflows/issue_moderator.yml
vendored
Normal file
14
.github/workflows/issue_moderator.yml
vendored
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
name: Issue moderator
|
||||||
|
|
||||||
|
on:
|
||||||
|
issue_comment:
|
||||||
|
types: [created]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
moderate:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Moderate issues
|
||||||
|
uses: tachiyomiorg/issue-moderator-action@v1.1
|
||||||
|
with:
|
||||||
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
19
.github/workflows/lock.yml
vendored
Normal file
19
.github/workflows/lock.yml
vendored
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
name: Lock threads
|
||||||
|
|
||||||
|
on:
|
||||||
|
# Daily
|
||||||
|
schedule:
|
||||||
|
- cron: '0 * * * *'
|
||||||
|
# Manual trigger
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lock:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: dessant/lock-threads@v2
|
||||||
|
with:
|
||||||
|
github-token: ${{ github.token }}
|
||||||
|
issue-lock-inactive-days: '2'
|
||||||
|
pr-lock-inactive-days: '2'
|
126
CODE_OF_CONDUCT.md
Normal file
126
CODE_OF_CONDUCT.md
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
# Contributor Covenant Code of Conduct
|
||||||
|
|
||||||
|
## Our Pledge
|
||||||
|
|
||||||
|
We as members, contributors, and leaders pledge to make participation in our
|
||||||
|
community a harassment-free experience for everyone, regardless of age, body
|
||||||
|
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||||
|
identity and expression, level of experience, education, socio-economic status,
|
||||||
|
nationality, personal appearance, race, caste, color, religion, or sexual identity
|
||||||
|
and orientation.
|
||||||
|
|
||||||
|
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||||
|
diverse, inclusive, and healthy community.
|
||||||
|
|
||||||
|
## Our Standards
|
||||||
|
|
||||||
|
Examples of behavior that contributes to a positive environment for our
|
||||||
|
community include:
|
||||||
|
|
||||||
|
* Demonstrating empathy and kindness toward other people
|
||||||
|
* Being respectful of differing opinions, viewpoints, and experiences
|
||||||
|
* Giving and gracefully accepting constructive feedback
|
||||||
|
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||||
|
and learning from the experience
|
||||||
|
* Focusing on what is best not just for us as individuals, but for the
|
||||||
|
overall community
|
||||||
|
|
||||||
|
Examples of unacceptable behavior include:
|
||||||
|
|
||||||
|
* The use of sexualized language or imagery, and sexual attention or
|
||||||
|
advances of any kind
|
||||||
|
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||||
|
* Public or private harassment
|
||||||
|
* Publishing others' private information, such as a physical or email
|
||||||
|
address, without their explicit permission
|
||||||
|
* Other conduct which could reasonably be considered inappropriate in a
|
||||||
|
professional setting
|
||||||
|
|
||||||
|
## Enforcement Responsibilities
|
||||||
|
|
||||||
|
Community moderators are responsible for clarifying and enforcing our standards of
|
||||||
|
acceptable behavior and will take appropriate and fair corrective action in
|
||||||
|
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||||
|
or harmful.
|
||||||
|
|
||||||
|
Community moderators have the right and responsibility to remove, edit, or reject
|
||||||
|
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||||
|
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||||
|
decisions when appropriate.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This Code of Conduct applies within all community spaces, and also applies when
|
||||||
|
an individual is officially representing the community in public spaces.
|
||||||
|
Examples of representing our community include using an official e-mail address,
|
||||||
|
posting via an official social media account, or acting as an appointed
|
||||||
|
representative at an online or offline event.
|
||||||
|
|
||||||
|
## Enforcement
|
||||||
|
|
||||||
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||||
|
reported to the community moderators responsible for enforcement at
|
||||||
|
the [Tachiyomi Discord server](https://discord.gg/tachiyomi).
|
||||||
|
All complaints will be reviewed and investigated promptly and fairly.
|
||||||
|
|
||||||
|
All community moderators are obligated to respect the privacy and security of the
|
||||||
|
reporter of any incident.
|
||||||
|
|
||||||
|
## Enforcement Guidelines
|
||||||
|
|
||||||
|
Community moderators will follow these Community Impact Guidelines in determining
|
||||||
|
the consequences for any action they deem in violation of this Code of Conduct:
|
||||||
|
|
||||||
|
### 1. Correction
|
||||||
|
|
||||||
|
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||||
|
unprofessional or unwelcome in the community.
|
||||||
|
|
||||||
|
**Consequence**: A private, written warning from community moderators, providing
|
||||||
|
clarity around the nature of the violation and an explanation of why the
|
||||||
|
behavior was inappropriate. A public apology may be requested.
|
||||||
|
|
||||||
|
### 2. Warning
|
||||||
|
|
||||||
|
**Community Impact**: A violation through a single incident or series
|
||||||
|
of actions.
|
||||||
|
|
||||||
|
**Consequence**: A warning with consequences for continued behavior. No
|
||||||
|
interaction with the people involved, including unsolicited interaction with
|
||||||
|
those enforcing the Code of Conduct, for a specified period of time. This
|
||||||
|
includes avoiding interactions in community spaces as well as external channels
|
||||||
|
like social media. Violating these terms may lead to a temporary or
|
||||||
|
permanent ban.
|
||||||
|
|
||||||
|
### 3. Temporary Ban
|
||||||
|
|
||||||
|
**Community Impact**: A serious violation of community standards, including
|
||||||
|
sustained inappropriate behavior.
|
||||||
|
|
||||||
|
**Consequence**: A temporary ban from any sort of interaction or public
|
||||||
|
communication with the community for a specified period of time. No public or
|
||||||
|
private interaction with the people involved, including unsolicited interaction
|
||||||
|
with those enforcing the Code of Conduct, is allowed during this period.
|
||||||
|
Violating these terms may lead to a permanent ban.
|
||||||
|
|
||||||
|
### 4. Permanent Ban
|
||||||
|
|
||||||
|
**Community Impact**: Demonstrating a pattern of violation of community
|
||||||
|
standards, including sustained inappropriate behavior, harassment of an
|
||||||
|
individual, or aggression toward or disparagement of classes of individuals.
|
||||||
|
|
||||||
|
**Consequence**: A permanent ban from any sort of public interaction within
|
||||||
|
the community.
|
||||||
|
|
||||||
|
## Attribution
|
||||||
|
|
||||||
|
This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org/),
|
||||||
|
version 2.1, available at
|
||||||
|
[v2.1](https://www.contributor-covenant.org/version/2/1/code_of_conduct.html).
|
||||||
|
|
||||||
|
Community Impact Guidelines were inspired by
|
||||||
|
[Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity).
|
||||||
|
|
||||||
|
For answers to common questions about this code of conduct, see the FAQ at
|
||||||
|
[FAQ](https://www.contributor-covenant.org/faq). Translations are available
|
||||||
|
at [translations](https://www.contributor-covenant.org/translations).
|
@ -26,7 +26,7 @@ When creating a fork, remember to:
|
|||||||
- To avoid confusion with the main app:
|
- To avoid confusion with the main app:
|
||||||
- Change the app name
|
- Change the app name
|
||||||
- Change the app icon
|
- Change the app icon
|
||||||
- Change or disable the [app update checker](https://github.com/tachiyomiorg/tachiyomi/blob/master/app/src/main/java/eu/kanade/tachiyomi/data/updater/github/GithubUpdateChecker.kt)
|
- Change or disable the [app update checker](https://github.com/tachiyomiorg/tachiyomi/blob/master/app/src/main/java/eu/kanade/tachiyomi/data/updater/GithubUpdateChecker.kt)
|
||||||
- To avoid installation conflicts:
|
- To avoid installation conflicts:
|
||||||
- Change the `applicationId` in [`build.gradle.kts`](https://github.com/tachiyomiorg/tachiyomi/blob/master/app/build.gradle.kts)
|
- Change the `applicationId` in [`build.gradle.kts`](https://github.com/tachiyomiorg/tachiyomi/blob/master/app/build.gradle.kts)
|
||||||
- To avoid having your data polluting the main app's analytics and crash report services:
|
- To avoid having your data polluting the main app's analytics and crash report services:
|
||||||
|
@ -1,31 +0,0 @@
|
|||||||
### r2903
|
|
||||||
- The MyAnimeList tracker was rewritten. You will need to log out and log in again.
|
|
||||||
|
|
||||||
### 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).
|
|
24
README.md
24
README.md
@ -1,20 +1,18 @@
|
|||||||
| Build | Stable | Weekly Preview | Contribute | Support Server |
|
| Build | Stable | Weekly Preview | Contribute | Support Server |
|
||||||
|-------|----------|---------|------------|---------|
|
|-------|----------|---------|------------|---------|
|
||||||
|  | [](https://github.com/tachiyomiorg/tachiyomi/releases) | [](https://github.com/tachiyomiorg/tachiyomi-preview/releases) | [](https://hosted.weblate.org/engage/tachiyomi/?utm_source=widget) | [](https://discord.gg/tachiyomi) |
|
|  | [](https://github.com/tachiyomiorg/tachiyomi/releases) | [](https://github.com/tachiyomiorg/tachiyomi-preview/releases) | [](https://hosted.weblate.org/engage/tachiyomi/?utm_source=widget) | [](https://discord.gg/tachiyomi) |
|
||||||
|
|
||||||
|
|
||||||
# Tachiyomi
|
# Tachiyomi
|
||||||
Tachiyomi is a free and open source manga reader for Android 5.0 and above.
|
Tachiyomi is a free and open source manga reader for Android 6.0 and above.
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
Features include:
|
Features include:
|
||||||
* Online reading from sources such as MangaDex, MangaSee, Mangakakalot, [and more](https://github.com/tachiyomiorg/tachiyomi-extensions)
|
* Online reading from a variety of sources
|
||||||
* Local reading of downloaded manga
|
* Local reading of downloaded content
|
||||||
* A configurable reader with multiple viewers, reading directions and other settings.
|
* A configurable reader with multiple viewers, reading directions and other settings.
|
||||||
* [MyAnimeList](https://myanimelist.net/), [AniList](https://anilist.co/), [Kitsu](https://kitsu.io/), [Shikimori](https://shikimori.one), and [Bangumi](https://bgm.tv/) support
|
* Tracker support: [MyAnimeList](https://myanimelist.net/), [AniList](https://anilist.co/), [Kitsu](https://kitsu.io/), [Shikimori](https://shikimori.one), and [Bangumi](https://bgm.tv/)
|
||||||
* Categories to organize your library
|
* Categories to organize your library
|
||||||
* Light and dark themes
|
* Light and dark themes
|
||||||
* Schedule updating your library for new chapters
|
* Schedule updating your library for new chapters
|
||||||
@ -23,7 +21,7 @@ Features include:
|
|||||||
## Download
|
## Download
|
||||||
Get the app from our [releases page](https://github.com/tachiyomiorg/tachiyomi/releases).
|
Get the app from our [releases page](https://github.com/tachiyomiorg/tachiyomi/releases).
|
||||||
|
|
||||||
If you want to try new features before they get to the stable release, you can download the preview version [here](https://github.com/tachiyomiorg/android-app-preview/releases).
|
If you want to try new features before they get to the stable release, you can download the preview version [here](https://github.com/tachiyomiorg/tachiyomi-preview/releases).
|
||||||
|
|
||||||
## Issues, Feature Requests and Contributing
|
## Issues, Feature Requests and Contributing
|
||||||
|
|
||||||
@ -38,13 +36,12 @@ Please make sure to read the full guidelines. Your issue may be closed without w
|
|||||||
|
|
||||||
<details><summary>Bugs</summary>
|
<details><summary>Bugs</summary>
|
||||||
|
|
||||||
* Include version (More > About > Version)
|
* Include version (More → About → Version)
|
||||||
* If not latest, try updating, it may have already been solved
|
* If not latest, try updating, it may have already been solved
|
||||||
* Preview version is equal to the number of commits as seen in the main page
|
* Preview version is equal to the number of commits as seen in the main page
|
||||||
* Include steps to reproduce (if not obvious from description)
|
* Include steps to reproduce (if not obvious from description)
|
||||||
* Include screenshot (if needed)
|
* Include screenshot (if needed)
|
||||||
* If it could be device-dependent, try reproducing on another device (if possible)
|
* If it could be device-dependent, try reproducing on another device (if possible)
|
||||||
* For large logs use http://pastebin.com/ (or similar)
|
|
||||||
* Don't group unrelated requests into one issue
|
* Don't group unrelated requests into one issue
|
||||||
|
|
||||||
DO: https://github.com/tachiyomiorg/tachiyomi/issues/24 https://github.com/tachiyomiorg/tachiyomi/issues/71
|
DO: https://github.com/tachiyomiorg/tachiyomi/issues/24 https://github.com/tachiyomiorg/tachiyomi/issues/71
|
||||||
@ -63,7 +60,12 @@ Source requests should be created at https://github.com/tachiyomiorg/tachiyomi-e
|
|||||||
|
|
||||||
<details><summary>Contributing</summary>
|
<details><summary>Contributing</summary>
|
||||||
|
|
||||||
See [CONTRIBUTING.md](https://github.com/tachiyomiorg/tachiyomi/blob/master/CONTRIBUTING.md).
|
See [CONTRIBUTING.md](./CONTRIBUTING.md).
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details><summary>Code of Conduct</summary>
|
||||||
|
|
||||||
|
See [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md).
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
## FAQ
|
## FAQ
|
||||||
|
@ -8,7 +8,6 @@ plugins {
|
|||||||
id("com.android.application")
|
id("com.android.application")
|
||||||
id("com.mikepenz.aboutlibraries.plugin")
|
id("com.mikepenz.aboutlibraries.plugin")
|
||||||
kotlin("android")
|
kotlin("android")
|
||||||
kotlin("kapt")
|
|
||||||
kotlin("plugin.serialization")
|
kotlin("plugin.serialization")
|
||||||
id("com.github.zellius.shortcut-helper")
|
id("com.github.zellius.shortcut-helper")
|
||||||
}
|
}
|
||||||
@ -19,18 +18,19 @@ if (gradle.startParameter.taskRequests.toString().contains("Standard")) {
|
|||||||
|
|
||||||
shortcutHelper.setFilePath("./shortcuts.xml")
|
shortcutHelper.setFilePath("./shortcuts.xml")
|
||||||
|
|
||||||
|
val SUPPORTED_ABIS = setOf("armeabi-v7a", "arm64-v8a", "x86")
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion(AndroidConfig.compileSdk)
|
compileSdk = AndroidConfig.compileSdk
|
||||||
buildToolsVersion(AndroidConfig.buildTools)
|
|
||||||
ndkVersion = AndroidConfig.ndk
|
ndkVersion = AndroidConfig.ndk
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "eu.kanade.tachiyomi"
|
applicationId = "eu.kanade.tachiyomi"
|
||||||
minSdkVersion(AndroidConfig.minSdk)
|
minSdk = AndroidConfig.minSdk
|
||||||
targetSdkVersion(AndroidConfig.targetSdk)
|
targetSdk = AndroidConfig.targetSdk
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
versionCode = 56
|
versionCode = 69
|
||||||
versionName = "0.10.9"
|
versionName = "0.12.3"
|
||||||
|
|
||||||
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
||||||
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
|
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
|
||||||
@ -40,64 +40,78 @@ android {
|
|||||||
// Please disable ACRA or use your own instance in forked versions of the project
|
// Please disable ACRA or use your own instance in forked versions of the project
|
||||||
buildConfigField("String", "ACRA_URI", "\"https://tachiyomi.kanade.eu/crash_report\"")
|
buildConfigField("String", "ACRA_URI", "\"https://tachiyomi.kanade.eu/crash_report\"")
|
||||||
|
|
||||||
multiDexEnabled = true
|
|
||||||
|
|
||||||
ndk {
|
ndk {
|
||||||
abiFilters += setOf("armeabi-v7a", "arm64-v8a", "x86")
|
abiFilters += SUPPORTED_ABIS
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
buildFeatures {
|
splits {
|
||||||
viewBinding = true
|
abi {
|
||||||
|
isEnable = true
|
||||||
|
reset()
|
||||||
|
include(*SUPPORTED_ABIS.toTypedArray())
|
||||||
|
isUniversalApk = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
named("debug") {
|
named("debug") {
|
||||||
versionNameSuffix = "-${getCommitCount()}"
|
versionNameSuffix = "-${getCommitCount()}"
|
||||||
applicationIdSuffix = ".debug"
|
applicationIdSuffix = ".debug"
|
||||||
|
|
||||||
|
isShrinkResources = true
|
||||||
|
isMinifyEnabled = true
|
||||||
|
proguardFiles("proguard-android-optimize.txt", "proguard-rules.pro")
|
||||||
|
}
|
||||||
|
create("debugFull") { // Debug without R8
|
||||||
|
initWith(getByName("debug"))
|
||||||
|
isShrinkResources = false
|
||||||
|
isMinifyEnabled = false
|
||||||
}
|
}
|
||||||
named("release") {
|
named("release") {
|
||||||
/*named("postprocessing") {
|
isShrinkResources = true
|
||||||
postprocessing {
|
isMinifyEnabled = true
|
||||||
isObfuscate = false
|
proguardFiles("proguard-android-optimize.txt", "proguard-rules.pro")
|
||||||
isOptimizeCode = true
|
|
||||||
isRemoveUnusedCode = false
|
|
||||||
isRemoveUnusedResources = true
|
|
||||||
}
|
|
||||||
setProguardFiles(listOf(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"))
|
|
||||||
}*/
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
flavorDimensions("default")
|
sourceSets {
|
||||||
|
getByName("debugFull").res.srcDirs("src/debug/res")
|
||||||
|
}
|
||||||
|
|
||||||
|
flavorDimensions.add("default")
|
||||||
|
|
||||||
productFlavors {
|
productFlavors {
|
||||||
create("standard") {
|
create("standard") {
|
||||||
buildConfigField("boolean", "INCLUDE_UPDATER", "true")
|
buildConfigField("boolean", "INCLUDE_UPDATER", "true")
|
||||||
dimension = "default"
|
dimension = "default"
|
||||||
}
|
}
|
||||||
create("fdroid") {
|
|
||||||
dimension = "default"
|
|
||||||
}
|
|
||||||
create("dev") {
|
create("dev") {
|
||||||
resConfigs("en", "xxhdpi")
|
resourceConfigurations.addAll(listOf("en", "xxhdpi"))
|
||||||
dimension = "default"
|
dimension = "default"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
packagingOptions {
|
packagingOptions {
|
||||||
exclude("META-INF/DEPENDENCIES")
|
resources.excludes.addAll(listOf(
|
||||||
exclude("LICENSE.txt")
|
"META-INF/DEPENDENCIES",
|
||||||
exclude("META-INF/LICENSE")
|
"LICENSE.txt",
|
||||||
exclude("META-INF/LICENSE.txt")
|
"META-INF/LICENSE",
|
||||||
exclude("META-INF/NOTICE")
|
"META-INF/LICENSE.txt",
|
||||||
|
"META-INF/NOTICE",
|
||||||
|
"META-INF/*.kotlin_module",
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
dependenciesInfo {
|
dependenciesInfo {
|
||||||
includeInApk = false
|
includeInApk = false
|
||||||
}
|
}
|
||||||
|
|
||||||
lintOptions {
|
buildFeatures {
|
||||||
|
viewBinding = true
|
||||||
|
}
|
||||||
|
|
||||||
|
lint {
|
||||||
disable("MissingTranslation", "ExtraTranslation")
|
disable("MissingTranslation", "ExtraTranslation")
|
||||||
isAbortOnError = false
|
isAbortOnError = false
|
||||||
isCheckReleaseBuilds = false
|
isCheckReleaseBuilds = false
|
||||||
@ -114,79 +128,81 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
implementation(kotlin("reflect", version = BuildPluginsVersion.KOTLIN))
|
||||||
|
|
||||||
|
val coroutinesVersion = "1.5.2"
|
||||||
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion")
|
||||||
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion")
|
||||||
|
|
||||||
// Source models and interfaces from Tachiyomi 1.x
|
// Source models and interfaces from Tachiyomi 1.x
|
||||||
implementation("tachiyomi.sourceapi:source-api:1.1")
|
implementation("org.tachiyomi:source-api:1.1")
|
||||||
|
|
||||||
// AndroidX libraries
|
// AndroidX libraries
|
||||||
implementation("androidx.annotation:annotation:1.2.0-beta01")
|
implementation("androidx.annotation:annotation:1.3.0-beta01")
|
||||||
implementation("androidx.appcompat:appcompat:1.3.0-beta01")
|
implementation("androidx.appcompat:appcompat:1.4.0-alpha03")
|
||||||
implementation("androidx.biometric:biometric-ktx:1.2.0-alpha02")
|
implementation("androidx.biometric:biometric-ktx:1.2.0-alpha03")
|
||||||
implementation("androidx.browser:browser:1.3.0")
|
implementation("androidx.browser:browser:1.4.0-beta01")
|
||||||
implementation("androidx.cardview:cardview:1.0.0")
|
implementation("androidx.constraintlayout:constraintlayout:2.1.1")
|
||||||
implementation("androidx.constraintlayout:constraintlayout:2.1.0-alpha2")
|
|
||||||
implementation("androidx.coordinatorlayout:coordinatorlayout:1.1.0")
|
implementation("androidx.coordinatorlayout:coordinatorlayout:1.1.0")
|
||||||
implementation("androidx.core:core-ktx:1.5.0-beta01")
|
implementation("androidx.core:core-ktx:1.7.0-beta02")
|
||||||
implementation("androidx.multidex:multidex:2.0.1")
|
implementation("androidx.core:core-splashscreen:1.0.0-alpha02")
|
||||||
implementation("androidx.preference:preference-ktx:1.1.1")
|
implementation("androidx.recyclerview:recyclerview:1.3.0-alpha01")
|
||||||
implementation("androidx.recyclerview:recyclerview:1.2.0-beta01")
|
|
||||||
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01")
|
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01")
|
||||||
|
implementation("androidx.viewpager:viewpager:1.1.0-alpha01")
|
||||||
|
|
||||||
val lifecycleVersion = "2.3.0-rc01"
|
val lifecycleVersion = "2.4.0-beta01"
|
||||||
implementation("androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion")
|
implementation("androidx.lifecycle:lifecycle-common:$lifecycleVersion")
|
||||||
implementation("androidx.lifecycle:lifecycle-process:$lifecycleVersion")
|
implementation("androidx.lifecycle:lifecycle-process:$lifecycleVersion")
|
||||||
implementation("androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion")
|
implementation("androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion")
|
||||||
|
|
||||||
// Job scheduling
|
// Job scheduling
|
||||||
implementation("androidx.work:work-runtime-ktx:2.5.0")
|
implementation("androidx.work:work-runtime-ktx:2.6.0")
|
||||||
|
|
||||||
// UI library
|
// RX
|
||||||
implementation("com.google.android.material:material:1.3.0")
|
|
||||||
|
|
||||||
"standardImplementation"("com.google.firebase:firebase-core:18.0.2")
|
|
||||||
|
|
||||||
// ReactiveX
|
|
||||||
implementation("io.reactivex:rxandroid:1.2.1")
|
implementation("io.reactivex:rxandroid:1.2.1")
|
||||||
implementation("io.reactivex:rxjava:1.3.8")
|
implementation("io.reactivex:rxjava:1.3.8")
|
||||||
implementation("com.jakewharton.rxrelay:rxrelay:1.2.0")
|
implementation("com.jakewharton.rxrelay:rxrelay:1.2.0")
|
||||||
implementation("com.github.pwittchen:reactivenetwork:0.13.0")
|
implementation("ru.beryukhov:flowreactivenetwork:1.0.4")
|
||||||
|
|
||||||
// Network client
|
// Network client
|
||||||
val okhttpVersion = "4.10.0-RC1"
|
val okhttpVersion = "4.9.1"
|
||||||
implementation("com.squareup.okhttp3:okhttp:$okhttpVersion")
|
implementation("com.squareup.okhttp3:okhttp:$okhttpVersion")
|
||||||
implementation("com.squareup.okhttp3:logging-interceptor:$okhttpVersion")
|
implementation("com.squareup.okhttp3:logging-interceptor:$okhttpVersion")
|
||||||
implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:$okhttpVersion")
|
implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:$okhttpVersion")
|
||||||
implementation("com.squareup.okio:okio:2.10.0")
|
implementation("com.squareup.okio:okio:2.10.0")
|
||||||
|
|
||||||
// TLS 1.3 support for Android < 10
|
// TLS 1.3 support for Android < 10
|
||||||
implementation("org.conscrypt:conscrypt-android:2.5.1")
|
implementation("org.conscrypt:conscrypt-android:2.5.2")
|
||||||
|
|
||||||
// JSON
|
// Data serialization (JSON, protobuf)
|
||||||
val kotlinSerializationVersion = "1.0.1"
|
val kotlinSerializationVersion = "1.3.0"
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinSerializationVersion")
|
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinSerializationVersion")
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-protobuf:$kotlinSerializationVersion")
|
implementation("org.jetbrains.kotlinx:kotlinx-serialization-protobuf:$kotlinSerializationVersion")
|
||||||
implementation("com.google.code.gson:gson:2.8.6")
|
|
||||||
|
// TODO: remove these once they're no longer used in any extensions
|
||||||
|
implementation("com.google.code.gson:gson:2.8.7")
|
||||||
implementation("com.github.salomonbrys.kotson:kotson:2.5.0")
|
implementation("com.github.salomonbrys.kotson:kotson:2.5.0")
|
||||||
|
|
||||||
// JavaScript engine
|
// JavaScript engine
|
||||||
implementation("com.squareup.duktape:duktape-android:1.3.0")
|
implementation("com.squareup.duktape:duktape-android:1.4.0")
|
||||||
|
|
||||||
|
// HTML parser
|
||||||
|
implementation("org.jsoup:jsoup:1.14.2")
|
||||||
|
|
||||||
// Disk
|
// Disk
|
||||||
implementation("com.jakewharton:disklrucache:2.0.2")
|
implementation("com.jakewharton:disklrucache:2.0.2")
|
||||||
implementation("com.github.inorichi:unifile:e9ee588")
|
implementation("com.github.tachiyomiorg:unifile:17bec43")
|
||||||
implementation("com.github.junrar:junrar:7.4.0")
|
implementation("com.github.junrar:junrar:7.4.0")
|
||||||
|
|
||||||
// HTML parser
|
|
||||||
implementation("org.jsoup:jsoup:1.13.1")
|
|
||||||
|
|
||||||
// Database
|
// Database
|
||||||
implementation("androidx.sqlite:sqlite-ktx:2.1.0")
|
implementation("androidx.sqlite:sqlite-ktx:2.1.0")
|
||||||
implementation("com.github.inorichi.storio:storio-common:8be19de@aar")
|
implementation("com.github.inorichi.storio:storio-common:8be19de@aar")
|
||||||
implementation("com.github.inorichi.storio:storio-sqlite:8be19de@aar")
|
implementation("com.github.inorichi.storio:storio-sqlite:8be19de@aar")
|
||||||
implementation("io.requery:sqlite-android:3.33.0")
|
implementation("com.github.requery:sqlite-android:3.36.0")
|
||||||
|
|
||||||
// Preferences
|
// Preferences
|
||||||
implementation("com.github.tfcporciuncula.flow-preferences:flow-preferences:1.3.3")
|
implementation("androidx.preference:preference-ktx:1.1.1")
|
||||||
|
implementation("com.github.tfcporciuncula.flow-preferences:flow-preferences:1.4.0")
|
||||||
|
|
||||||
// Model View Presenter
|
// Model View Presenter
|
||||||
val nucleusVersion = "3.0.0"
|
val nucleusVersion = "3.0.0"
|
||||||
@ -196,76 +212,71 @@ dependencies {
|
|||||||
// Dependency injection
|
// Dependency injection
|
||||||
implementation("com.github.inorichi.injekt:injekt-core:65b0440")
|
implementation("com.github.inorichi.injekt:injekt-core:65b0440")
|
||||||
|
|
||||||
// Image library
|
// Image loading
|
||||||
val glideVersion = "4.11.0"
|
val coilVersion = "1.3.2"
|
||||||
implementation("com.github.bumptech.glide:glide:$glideVersion")
|
implementation("io.coil-kt:coil:$coilVersion")
|
||||||
implementation("com.github.bumptech.glide:okhttp3-integration:$glideVersion")
|
implementation("io.coil-kt:coil-gif:$coilVersion")
|
||||||
kapt("com.github.bumptech.glide:compiler:$glideVersion")
|
|
||||||
|
|
||||||
implementation("com.github.tachiyomiorg:subsampling-scale-image-view:6caf219")
|
implementation("com.github.tachiyomiorg:subsampling-scale-image-view:846abe0") {
|
||||||
// TODO: switch to new decoder for stable releases
|
exclude(module = "image-decoder")
|
||||||
// implementation("com.github.tachiyomiorg:subsampling-scale-image-view:ca26317")
|
}
|
||||||
|
implementation("com.github.tachiyomiorg:image-decoder:7481a4a")
|
||||||
// Logging
|
|
||||||
implementation("com.jakewharton.timber:timber:4.7.1")
|
|
||||||
|
|
||||||
// Crash reports
|
|
||||||
implementation("ch.acra:acra-http:5.7.0")
|
|
||||||
|
|
||||||
// Sort
|
// Sort
|
||||||
implementation("com.github.gpanther:java-nat-sort:natural-comparator-1.1")
|
implementation("com.github.gpanther:java-nat-sort:natural-comparator-1.1")
|
||||||
|
|
||||||
// UI
|
// UI libraries
|
||||||
implementation("com.dmitrymalkovich.android:material-design-dimens:1.4")
|
implementation("com.google.android.material:material:1.5.0-alpha04")
|
||||||
implementation("com.github.dmytrodanylyk.android-process-button:library:1.0.4")
|
implementation("com.github.dmytrodanylyk.android-process-button:library:1.0.4")
|
||||||
implementation("eu.davidea:flexible-adapter:5.1.0")
|
implementation("eu.davidea:flexible-adapter:5.1.0")
|
||||||
implementation("eu.davidea:flexible-adapter-ui:1.0.0")
|
implementation("eu.davidea:flexible-adapter-ui:1.0.0")
|
||||||
implementation("com.nightlynexus.viewstatepageradapter:viewstatepageradapter:1.1.0")
|
implementation("com.nightlynexus.viewstatepageradapter:viewstatepageradapter:1.1.0")
|
||||||
implementation("com.github.chrisbanes:PhotoView:2.3.0")
|
implementation("com.github.chrisbanes:PhotoView:2.3.0")
|
||||||
implementation("com.github.tachiyomiorg:DirectionalViewPager:7d0617d")
|
implementation("com.github.tachiyomiorg:DirectionalViewPager:1.0.0") {
|
||||||
|
exclude(group = "androidx.viewpager", module = "viewpager")
|
||||||
// 3.2.0+ introduces weird UI blinking or cut off issues on some devices
|
}
|
||||||
val materialDialogsVersion = "3.1.1"
|
implementation("dev.chrisbanes.insetter:insetter:0.6.0")
|
||||||
implementation("com.afollestad.material-dialogs:core:$materialDialogsVersion")
|
|
||||||
implementation("com.afollestad.material-dialogs:input:$materialDialogsVersion")
|
|
||||||
implementation("com.afollestad.material-dialogs:datetime:$materialDialogsVersion")
|
|
||||||
|
|
||||||
// Conductor
|
// Conductor
|
||||||
implementation("com.bluelinelabs:conductor:2.1.5")
|
val conductorVersion = "3.0.0"
|
||||||
implementation("com.bluelinelabs:conductor-support:2.1.5") {
|
implementation("com.bluelinelabs:conductor:$conductorVersion")
|
||||||
exclude(group = "com.android.support")
|
implementation("com.bluelinelabs:conductor-viewpager:$conductorVersion")
|
||||||
}
|
implementation("com.github.tachiyomiorg:conductor-support-preference:$conductorVersion")
|
||||||
implementation("com.github.tachiyomiorg:conductor-support-preference:1.1.1")
|
|
||||||
|
|
||||||
// FlowBinding
|
// FlowBinding
|
||||||
val flowbindingVersion = "0.12.0"
|
val flowbindingVersion = "1.2.0"
|
||||||
implementation("io.github.reactivecircus.flowbinding:flowbinding-android:$flowbindingVersion")
|
implementation("io.github.reactivecircus.flowbinding:flowbinding-android:$flowbindingVersion")
|
||||||
implementation("io.github.reactivecircus.flowbinding:flowbinding-appcompat:$flowbindingVersion")
|
implementation("io.github.reactivecircus.flowbinding:flowbinding-appcompat:$flowbindingVersion")
|
||||||
implementation("io.github.reactivecircus.flowbinding:flowbinding-recyclerview:$flowbindingVersion")
|
implementation("io.github.reactivecircus.flowbinding:flowbinding-recyclerview:$flowbindingVersion")
|
||||||
implementation("io.github.reactivecircus.flowbinding:flowbinding-swiperefreshlayout:$flowbindingVersion")
|
implementation("io.github.reactivecircus.flowbinding:flowbinding-swiperefreshlayout:$flowbindingVersion")
|
||||||
implementation("io.github.reactivecircus.flowbinding:flowbinding-viewpager:$flowbindingVersion")
|
implementation("io.github.reactivecircus.flowbinding:flowbinding-viewpager:$flowbindingVersion")
|
||||||
|
|
||||||
|
// Logging
|
||||||
|
implementation("com.jakewharton.timber:timber:5.0.1")
|
||||||
|
|
||||||
|
// Crash reports/analytics
|
||||||
|
implementation("ch.acra:acra-http:5.8.1")
|
||||||
|
"standardImplementation"("com.google.firebase:firebase-analytics:19.0.1")
|
||||||
|
|
||||||
// Licenses
|
// Licenses
|
||||||
implementation("com.mikepenz:aboutlibraries:${BuildPluginsVersion.ABOUTLIB_PLUGIN}")
|
implementation("com.mikepenz:aboutlibraries-core:${BuildPluginsVersion.ABOUTLIB_PLUGIN}")
|
||||||
|
|
||||||
|
// Shizuku
|
||||||
|
val shizukuVersion = "12.0.0"
|
||||||
|
implementation("dev.rikka.shizuku:api:$shizukuVersion")
|
||||||
|
implementation("dev.rikka.shizuku:provider:$shizukuVersion")
|
||||||
|
|
||||||
// Tests
|
// Tests
|
||||||
testImplementation("junit:junit:4.13.1")
|
testImplementation("junit:junit:4.13.2")
|
||||||
testImplementation("org.assertj:assertj-core:3.16.1")
|
testImplementation("org.assertj:assertj-core:3.16.1")
|
||||||
testImplementation("org.mockito:mockito-core:1.10.19")
|
testImplementation("org.mockito:mockito-core:1.10.19")
|
||||||
|
|
||||||
val robolectricVersion = "3.1.4"
|
val robolectricVersion = "3.1.4"
|
||||||
testImplementation("org.robolectric:robolectric:$robolectricVersion")
|
testImplementation("org.robolectric:robolectric:$robolectricVersion")
|
||||||
testImplementation("org.robolectric:shadows-multidex:$robolectricVersion")
|
|
||||||
testImplementation("org.robolectric:shadows-play-services:$robolectricVersion")
|
testImplementation("org.robolectric:shadows-play-services:$robolectricVersion")
|
||||||
|
|
||||||
implementation(kotlin("reflect", version = BuildPluginsVersion.KOTLIN))
|
|
||||||
|
|
||||||
val coroutinesVersion = "1.4.2"
|
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion")
|
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion")
|
|
||||||
|
|
||||||
// For detecting memory leaks; see https://square.github.io/leakcanary/
|
// For detecting memory leaks; see https://square.github.io/leakcanary/
|
||||||
// debugImplementation("com.squareup.leakcanary:leakcanary-android:2.6")
|
// debugImplementation("com.squareup.leakcanary:leakcanary-android:2.7")
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks {
|
tasks {
|
||||||
@ -278,7 +289,8 @@ tasks {
|
|||||||
"-Xuse-experimental=kotlinx.coroutines.FlowPreview",
|
"-Xuse-experimental=kotlinx.coroutines.FlowPreview",
|
||||||
"-Xuse-experimental=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
"-Xuse-experimental=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
||||||
"-Xuse-experimental=kotlinx.coroutines.InternalCoroutinesApi",
|
"-Xuse-experimental=kotlinx.coroutines.InternalCoroutinesApi",
|
||||||
"-Xuse-experimental=kotlinx.serialization.ExperimentalSerializationApi"
|
"-Xuse-experimental=kotlinx.serialization.ExperimentalSerializationApi",
|
||||||
|
"-Xuse-experimental=coil.annotation.ExperimentalCoilApi",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
34
app/proguard-android-optimize.txt
Normal file
34
app/proguard-android-optimize.txt
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
-allowaccessmodification
|
||||||
|
-dontusemixedcaseclassnames
|
||||||
|
-verbose
|
||||||
|
|
||||||
|
-keepattributes *Annotation*
|
||||||
|
|
||||||
|
-keepclasseswithmembernames,includedescriptorclasses class * {
|
||||||
|
native <methods>;
|
||||||
|
}
|
||||||
|
|
||||||
|
-keepclassmembers enum * {
|
||||||
|
public static **[] values();
|
||||||
|
public static ** valueOf(java.lang.String);
|
||||||
|
}
|
||||||
|
|
||||||
|
-keepclassmembers class * implements android.os.Parcelable {
|
||||||
|
public static final ** CREATOR;
|
||||||
|
}
|
||||||
|
|
||||||
|
-keep class androidx.annotation.Keep
|
||||||
|
|
||||||
|
-keep @androidx.annotation.Keep class * {*;}
|
||||||
|
|
||||||
|
-keepclasseswithmembers class * {
|
||||||
|
@androidx.annotation.Keep <methods>;
|
||||||
|
}
|
||||||
|
|
||||||
|
-keepclasseswithmembers class * {
|
||||||
|
@androidx.annotation.Keep <fields>;
|
||||||
|
}
|
||||||
|
|
||||||
|
-keepclasseswithmembers class * {
|
||||||
|
@androidx.annotation.Keep <init>(...);
|
||||||
|
}
|
69
app/proguard-rules.pro
vendored
69
app/proguard-rules.pro
vendored
@ -1,29 +1,20 @@
|
|||||||
-dontobfuscate
|
-dontobfuscate
|
||||||
|
|
||||||
# Extensions may require methods unused in the core app
|
# Keep extension's common dependencies
|
||||||
-dontwarn eu.kanade.tachiyomi.**
|
-keep,allowoptimization class eu.kanade.tachiyomi.** { public protected *; }
|
||||||
-keep class eu.kanade.tachiyomi.** { public protected private *; }
|
-keep,allowoptimization class androidx.preference.** { *; }
|
||||||
|
-keep,allowoptimization class kotlin.** { public protected *; }
|
||||||
|
-keep,allowoptimization class kotlinx.coroutines.** { public protected *; }
|
||||||
|
-keep,allowoptimization class okhttp3.** { public protected *; }
|
||||||
|
-keep,allowoptimization class okio.** { public protected *; }
|
||||||
|
-keep,allowoptimization class rx.** { public protected *; }
|
||||||
|
-keep,allowoptimization class org.jsoup.** { public protected *; }
|
||||||
|
-keep,allowoptimization class com.google.gson.** { public protected *; }
|
||||||
|
-keep,allowoptimization class com.github.salomonbrys.kotson.** { public protected *; }
|
||||||
|
-keep,allowoptimization class com.squareup.duktape.** { public protected *; }
|
||||||
|
-keep,allowoptimization class uy.kohesive.injekt.** { public protected *; }
|
||||||
|
|
||||||
-keep class org.jsoup.** { *; }
|
##---------------Begin: proguard configuration for RxJava 1.x ----------
|
||||||
-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>();
|
|
||||||
}
|
|
||||||
|
|
||||||
# RxJava 1.1.0
|
|
||||||
-dontwarn sun.misc.**
|
-dontwarn sun.misc.**
|
||||||
|
|
||||||
-keepclassmembers class rx.internal.util.unsafe.*ArrayQueue*Field* {
|
-keepclassmembers class rx.internal.util.unsafe.*ArrayQueue*Field* {
|
||||||
@ -39,30 +30,38 @@
|
|||||||
rx.internal.util.atomic.LinkedQueueNode consumerNode;
|
rx.internal.util.atomic.LinkedQueueNode consumerNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
# ReactiveNetwork
|
-dontnote rx.internal.util.PlatformDependent
|
||||||
-dontwarn com.github.pwittchen.reactivenetwork.**
|
##---------------End: proguard configuration for RxJava 1.x ----------
|
||||||
|
|
||||||
## GSON ##
|
|
||||||
|
|
||||||
|
##---------------Begin: proguard configuration for Gson ----------
|
||||||
# Gson uses generic type information stored in a class file when working with fields. Proguard
|
# Gson uses generic type information stored in a class file when working with fields. Proguard
|
||||||
# removes such information by default, so configure it to keep all of it.
|
# removes such information by default, so configure it to keep all of it.
|
||||||
-keepattributes Signature
|
-keepattributes Signature
|
||||||
|
|
||||||
# Gson specific classes
|
# For using GSON @Expose annotation
|
||||||
-keep class sun.misc.Unsafe { *; }
|
-keepattributes *Annotation*
|
||||||
|
|
||||||
# Prevent proguard from stripping interface information from TypeAdapterFactory,
|
# Gson specific classes
|
||||||
|
-dontwarn sun.misc.**
|
||||||
|
|
||||||
|
# Prevent proguard from stripping interface information from TypeAdapter, TypeAdapterFactory,
|
||||||
# JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter)
|
# JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter)
|
||||||
|
-keep class * extends com.google.gson.TypeAdapter
|
||||||
-keep class * implements com.google.gson.TypeAdapterFactory
|
-keep class * implements com.google.gson.TypeAdapterFactory
|
||||||
-keep class * implements com.google.gson.JsonSerializer
|
-keep class * implements com.google.gson.JsonSerializer
|
||||||
-keep class * implements com.google.gson.JsonDeserializer
|
-keep class * implements com.google.gson.JsonDeserializer
|
||||||
|
|
||||||
|
# Prevent R8 from leaving Data object members always null
|
||||||
|
-keepclassmembers,allowobfuscation class * {
|
||||||
|
@com.google.gson.annotations.SerializedName <fields>;
|
||||||
|
}
|
||||||
|
##---------------End: proguard configuration for Gson ----------
|
||||||
|
|
||||||
## kotlinx.serialization ##
|
##---------------Begin: proguard configuration for kotlinx.serialization ----------
|
||||||
|
|
||||||
-keepattributes *Annotation*, InnerClasses
|
-keepattributes *Annotation*, InnerClasses
|
||||||
-dontnote kotlinx.serialization.AnnotationsKt # core serialization annotations
|
-dontnote kotlinx.serialization.AnnotationsKt # core serialization annotations
|
||||||
|
|
||||||
|
# kotlinx-serialization-json specific. Add this if you have java.lang.NoClassDefFoundError kotlinx.serialization.json.JsonObjectSerializer
|
||||||
-keepclassmembers class kotlinx.serialization.json.** {
|
-keepclassmembers class kotlinx.serialization.json.** {
|
||||||
*** Companion;
|
*** Companion;
|
||||||
}
|
}
|
||||||
@ -77,3 +76,9 @@
|
|||||||
-keepclasseswithmembers class eu.kanade.tachiyomi.** {
|
-keepclasseswithmembers class eu.kanade.tachiyomi.** {
|
||||||
kotlinx.serialization.KSerializer serializer(...);
|
kotlinx.serialization.KSerializer serializer(...);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
-keep class kotlinx.serialization.**
|
||||||
|
-keepclassmembers class kotlinx.serialization.** {
|
||||||
|
<methods>;
|
||||||
|
}
|
||||||
|
##---------------End: proguard configuration for kotlinx.serialization ----------
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
android:shortcutDisabledMessage="@string/app_not_available"
|
android:shortcutDisabledMessage="@string/app_not_available"
|
||||||
android:shortcutId="show_recently_updated"
|
android:shortcutId="show_recently_updated"
|
||||||
android:shortcutLongLabel="@string/label_recent_updates"
|
android:shortcutLongLabel="@string/label_recent_updates"
|
||||||
android:shortcutShortLabel="@string/short_recent_updates">
|
android:shortcutShortLabel="@string/label_recent_updates">
|
||||||
<intent
|
<intent
|
||||||
android:action="eu.kanade.tachiyomi.SHOW_RECENTLY_UPDATED"
|
android:action="eu.kanade.tachiyomi.SHOW_RECENTLY_UPDATED"
|
||||||
android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity" />
|
android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity" />
|
||||||
|
@ -1,34 +1,27 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:width="108dp"
|
android:width="108dp"
|
||||||
android:height="108dp"
|
android:height="108dp"
|
||||||
android:viewportWidth="108.0"
|
android:viewportWidth="108.0"
|
||||||
android:viewportHeight="108.0">
|
android:viewportHeight="108.0">
|
||||||
<path
|
<path
|
||||||
android:pathData="M14.5,7L86.5,7A7,7 0,0 1,93.5 14L93.5,95A7,7 0,0 1,86.5 102L14.5,102A7,7 0,0 1,7.5 95L7.5,14A7,7 0,0 1,14.5 7z"
|
android:pathData="M14.5,7L86.5,7A7,7 0,0 1,93.5 14L93.5,95A7,7 0,0 1,86.5 102L14.5,102A7,7 0,0 1,7.5 95L7.5,14A7,7 0,0 1,14.5 7z"
|
||||||
android:fillType="evenOdd"
|
|
||||||
android:fillColor="#000"/>
|
android:fillColor="#000"/>
|
||||||
<path
|
<path
|
||||||
android:pathData="M14.5,7L86.5,7A7,7 0,0 1,93.5 14L93.5,95A7,7 0,0 1,86.5 102L14.5,102A7,7 0,0 1,7.5 95L7.5,14A7,7 0,0 1,14.5 7z"
|
android:pathData="M14.5,7L86.5,7A7,7 0,0 1,93.5 14L93.5,95A7,7 0,0 1,86.5 102L14.5,102A7,7 0,0 1,7.5 95L7.5,14A7,7 0,0 1,14.5 7z"
|
||||||
android:fillType="evenOdd"
|
|
||||||
android:fillColor="#455A64"/>
|
android:fillColor="#455A64"/>
|
||||||
<path
|
<path
|
||||||
android:pathData="M7.5,12.01C7.5,9.24 9.74,7 12.5,7L17.5,7L17.5,102L12.5,102C9.74,102 7.5,99.77 7.5,96.99L7.5,12.01Z"
|
android:pathData="M7.5,12.01C7.5,9.24 9.74,7 12.5,7L17.5,7L17.5,102L12.5,102C9.74,102 7.5,99.77 7.5,96.99L7.5,12.01Z"
|
||||||
android:fillType="evenOdd"
|
|
||||||
android:fillColor="#607D8B"/>
|
android:fillColor="#607D8B"/>
|
||||||
<path
|
<path
|
||||||
android:pathData="M54,54.5m-25.5,0a25.5,25.5 0,1 1,51 0a25.5,25.5 0,1 1,-51 0"
|
android:pathData="M54,54.5m-25.5,0a25.5,25.5 0,1 1,51 0a25.5,25.5 0,1 1,-51 0"
|
||||||
android:fillType="evenOdd"
|
|
||||||
android:fillColor="#000"/>
|
android:fillColor="#000"/>
|
||||||
<path
|
<path
|
||||||
android:pathData="M54,54.5m-25.5,0a25.5,25.5 0,1 1,51 0a25.5,25.5 0,1 1,-51 0"
|
android:pathData="M54,54.5m-25.5,0a25.5,25.5 0,1 1,51 0a25.5,25.5 0,1 1,-51 0"
|
||||||
android:fillType="evenOdd"
|
|
||||||
android:fillColor="#CE2828"/>
|
android:fillColor="#CE2828"/>
|
||||||
<path
|
<path
|
||||||
android:pathData="M54,54.5m-19.94,0a19.94,19.94 0,1 1,39.87 0a19.94,19.94 0,1 1,-39.87 0"
|
android:pathData="M54,54.5m-19.94,0a19.94,19.94 0,1 1,39.87 0a19.94,19.94 0,1 1,-39.87 0"
|
||||||
android:fillType="evenOdd"
|
|
||||||
android:fillColor="#FFF"/>
|
android:fillColor="#FFF"/>
|
||||||
<path
|
<path
|
||||||
android:pathData="M52.04,46.3L47.42,46.3C46.14,46.3 44.93,46.23 44.2,46.14L44.2,49.76C45,49.65 46.16,49.6 47.42,49.6L60.58,49.6C61.86,49.6 63.02,49.65 63.82,49.76L63.82,46.14C63.09,46.23 61.86,46.3 60.58,46.3L55.69,46.3L55.69,45.07C55.69,44.43 55.73,43.95 55.82,43.45L51.9,43.45C51.99,44 52.04,44.43 52.04,45.07L52.04,46.3ZM46.78,60.68C45.46,60.68 44.29,60.63 43.45,60.52L43.45,64.14C44.34,64.03 45.46,63.98 46.78,63.98L61.29,63.98C62.57,63.98 63.71,64.03 64.57,64.14L64.57,60.52C63.73,60.63 62.57,60.68 61.29,60.68L58.24,60.68C59.33,58.06 59.99,56.23 60.7,53.91C61.34,51.81 61.34,51.81 61.56,51.13L57.58,50.06C57.51,50.93 57.37,51.52 56.89,53.41C56.19,56.14 55.32,58.74 54.5,60.68L46.78,60.68ZM46.48,51.36C47.55,54.02 48.28,56.53 49.03,60.15L52.66,58.9C51.65,54.98 50.92,52.66 49.94,50.11L46.48,51.36Z"
|
android:pathData="M52.04,46.3L47.42,46.3C46.14,46.3 44.93,46.23 44.2,46.14L44.2,49.76C45,49.65 46.16,49.6 47.42,49.6L60.58,49.6C61.86,49.6 63.02,49.65 63.82,49.76L63.82,46.14C63.09,46.23 61.86,46.3 60.58,46.3L55.69,46.3L55.69,45.07C55.69,44.43 55.73,43.95 55.82,43.45L51.9,43.45C51.99,44 52.04,44.43 52.04,45.07L52.04,46.3ZM46.78,60.68C45.46,60.68 44.29,60.63 43.45,60.52L43.45,64.14C44.34,64.03 45.46,63.98 46.78,63.98L61.29,63.98C62.57,63.98 63.71,64.03 64.57,64.14L64.57,60.52C63.73,60.63 62.57,60.68 61.29,60.68L58.24,60.68C59.33,58.06 59.99,56.23 60.7,53.91C61.34,51.81 61.34,51.81 61.56,51.13L57.58,50.06C57.51,50.93 57.37,51.52 56.89,53.41C56.19,56.14 55.32,58.74 54.5,60.68L46.78,60.68ZM46.48,51.36C47.55,54.02 48.28,56.53 49.03,60.15L52.66,58.9C51.65,54.98 50.92,52.66 49.94,50.11L46.48,51.36Z"
|
||||||
android:fillType="evenOdd"
|
|
||||||
android:fillColor="#000"/>
|
android:fillColor="#000"/>
|
||||||
</vector>
|
</vector>
|
||||||
|
@ -18,13 +18,13 @@
|
|||||||
<!-- For managing extensions -->
|
<!-- For managing extensions -->
|
||||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||||
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
|
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
|
||||||
|
<uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" />
|
||||||
<!-- To view extension packages in API 30+ -->
|
<!-- To view extension packages in API 30+ -->
|
||||||
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
|
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".App"
|
android:name=".App"
|
||||||
android:allowBackup="true"
|
android:allowBackup="false"
|
||||||
android:fullBackupContent="@xml/backup_rules"
|
|
||||||
android:hardwareAccelerated="true"
|
android:hardwareAccelerated="true"
|
||||||
android:hasFragileUserData="true"
|
android:hasFragileUserData="true"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
@ -32,12 +32,15 @@
|
|||||||
android:largeHeap="true"
|
android:largeHeap="true"
|
||||||
android:requestLegacyExternalStorage="true"
|
android:requestLegacyExternalStorage="true"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:theme="@style/Theme.Tachiyomi.Light"
|
android:theme="@style/Theme.Tachiyomi"
|
||||||
|
android:supportsRtl="true"
|
||||||
android:networkSecurityConfig="@xml/network_security_config">
|
android:networkSecurityConfig="@xml/network_security_config">
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.main.MainActivity"
|
android:name=".ui.main.MainActivity"
|
||||||
android:launchMode="singleTop"
|
android:launchMode="singleTop"
|
||||||
android:theme="@style/Theme.Splash">
|
android:theme="@style/Theme.Tachiyomi.SplashScreen"
|
||||||
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
@ -51,7 +54,8 @@
|
|||||||
android:name=".ui.main.DeepLinkActivity"
|
android:name=".ui.main.DeepLinkActivity"
|
||||||
android:launchMode="singleTask"
|
android:launchMode="singleTask"
|
||||||
android:theme="@android:style/Theme.NoDisplay"
|
android:theme="@android:style/Theme.NoDisplay"
|
||||||
android:label="@string/action_global_search">
|
android:label="@string/action_global_search"
|
||||||
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.SEARCH" />
|
<action android:name="android.intent.action.SEARCH" />
|
||||||
<action android:name="com.google.android.gms.actions.SEARCH_ACTION" />
|
<action android:name="com.google.android.gms.actions.SEARCH_ACTION" />
|
||||||
@ -72,9 +76,11 @@
|
|||||||
android:name="android.app.searchable"
|
android:name="android.app.searchable"
|
||||||
android:resource="@xml/searchable" />
|
android:resource="@xml/searchable" />
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.reader.ReaderActivity"
|
android:name=".ui.reader.ReaderActivity"
|
||||||
android:launchMode="singleTask">
|
android:launchMode="singleTask"
|
||||||
|
android:exported="false">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="com.samsung.android.support.REMOTE_ACTION" />
|
<action android:name="com.samsung.android.support.REMOTE_ACTION" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
@ -82,15 +88,26 @@
|
|||||||
<meta-data android:name="com.samsung.android.support.REMOTE_ACTION"
|
<meta-data android:name="com.samsung.android.support.REMOTE_ACTION"
|
||||||
android:resource="@xml/s_pen_actions"/>
|
android:resource="@xml/s_pen_actions"/>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.security.BiometricUnlockActivity"
|
android:name=".ui.security.UnlockActivity"
|
||||||
android:theme="@style/Theme.Splash" />
|
android:theme="@style/Theme.Tachiyomi"
|
||||||
|
android:exported="false" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.webview.WebViewActivity"
|
android:name=".ui.webview.WebViewActivity"
|
||||||
android:configChanges="uiMode|orientation|screenSize" />
|
android:configChanges="uiMode|orientation|screenSize"
|
||||||
|
android:exported="false" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".extension.util.ExtensionInstallActivity"
|
||||||
|
android:theme="@android:style/Theme.Translucent.NoTitleBar"
|
||||||
|
android:exported="false" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.setting.track.AnilistLoginActivity"
|
android:name=".ui.setting.track.AnilistLoginActivity"
|
||||||
android:label="Anilist">
|
android:label="Anilist"
|
||||||
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
@ -104,7 +121,8 @@
|
|||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.setting.track.MyAnimeListLoginActivity"
|
android:name=".ui.setting.track.MyAnimeListLoginActivity"
|
||||||
android:label="MyAnimeList">
|
android:label="MyAnimeList"
|
||||||
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
@ -118,7 +136,8 @@
|
|||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.setting.track.ShikimoriLoginActivity"
|
android:name=".ui.setting.track.ShikimoriLoginActivity"
|
||||||
android:label="Shikimori">
|
android:label="Shikimori"
|
||||||
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
@ -132,7 +151,8 @@
|
|||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.setting.track.BangumiLoginActivity"
|
android:name=".ui.setting.track.BangumiLoginActivity"
|
||||||
android:label="Bangumi">
|
android:label="Bangumi"
|
||||||
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
@ -145,20 +165,6 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<activity
|
|
||||||
android:name=".extension.util.ExtensionInstallActivity"
|
|
||||||
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
|
|
||||||
|
|
||||||
<provider
|
|
||||||
android:name="androidx.core.content.FileProvider"
|
|
||||||
android:authorities="${applicationId}.provider"
|
|
||||||
android:exported="false"
|
|
||||||
android:grantUriPermissions="true">
|
|
||||||
<meta-data
|
|
||||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
|
||||||
android:resource="@xml/provider_paths" />
|
|
||||||
</provider>
|
|
||||||
|
|
||||||
<receiver
|
<receiver
|
||||||
android:name=".data.notification.NotificationReceiver"
|
android:name=".data.notification.NotificationReceiver"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
@ -183,6 +189,32 @@
|
|||||||
android:name=".data.backup.BackupRestoreService"
|
android:name=".data.backup.BackupRestoreService"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
|
<service android:name=".extension.util.ExtensionInstallService"
|
||||||
|
android:exported="false" />
|
||||||
|
|
||||||
|
<provider
|
||||||
|
android:name="androidx.core.content.FileProvider"
|
||||||
|
android:authorities="${applicationId}.provider"
|
||||||
|
android:exported="false"
|
||||||
|
android:grantUriPermissions="true">
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
|
android:resource="@xml/provider_paths" />
|
||||||
|
</provider>
|
||||||
|
|
||||||
|
<provider
|
||||||
|
android:name="rikka.shizuku.ShizukuProvider"
|
||||||
|
android:authorities="${applicationId}.shizuku"
|
||||||
|
android:multiprocess="false"
|
||||||
|
android:enabled="true"
|
||||||
|
android:exported="true"
|
||||||
|
android:permission="android.permission.INTERACT_ACROSS_USERS_FULL" />
|
||||||
|
|
||||||
|
<meta-data android:name="android.webkit.WebView.EnableSafeBrowsing"
|
||||||
|
android:value="false" />
|
||||||
|
<meta-data android:name="android.webkit.WebView.MetricsOptOut"
|
||||||
|
android:value="true" />
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
@ -0,0 +1,100 @@
|
|||||||
|
package com.google.android.material.appbar
|
||||||
|
|
||||||
|
import android.animation.ValueAnimator
|
||||||
|
import android.view.View
|
||||||
|
import android.view.animation.DecelerateInterpolator
|
||||||
|
import androidx.appcompat.widget.Toolbar
|
||||||
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
|
import androidx.core.animation.doOnEnd
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
|
import androidx.core.view.marginTop
|
||||||
|
import eu.kanade.tachiyomi.util.system.animatorDurationScale
|
||||||
|
import eu.kanade.tachiyomi.util.view.findChild
|
||||||
|
import eu.kanade.tachiyomi.widget.ElevationAppBarLayout
|
||||||
|
import kotlin.math.roundToLong
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hide toolbar on scroll behavior for [AppBarLayout].
|
||||||
|
*
|
||||||
|
* Inside this package to access some package-private methods.
|
||||||
|
*/
|
||||||
|
class HideToolbarOnScrollBehavior : AppBarLayout.Behavior() {
|
||||||
|
|
||||||
|
@ViewCompat.NestedScrollType
|
||||||
|
private var lastStartedType: Int = 0
|
||||||
|
|
||||||
|
private var offsetAnimator: ValueAnimator? = null
|
||||||
|
|
||||||
|
private var toolbarHeight: Int = 0
|
||||||
|
|
||||||
|
override fun onStartNestedScroll(
|
||||||
|
parent: CoordinatorLayout,
|
||||||
|
child: AppBarLayout,
|
||||||
|
directTargetChild: View,
|
||||||
|
target: View,
|
||||||
|
nestedScrollAxes: Int,
|
||||||
|
type: Int
|
||||||
|
): Boolean {
|
||||||
|
lastStartedType = type
|
||||||
|
offsetAnimator?.cancel()
|
||||||
|
return super.onStartNestedScroll(parent, child, directTargetChild, target, nestedScrollAxes, type)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStopNestedScroll(
|
||||||
|
parent: CoordinatorLayout,
|
||||||
|
layout: AppBarLayout,
|
||||||
|
target: View,
|
||||||
|
type: Int
|
||||||
|
) {
|
||||||
|
super.onStopNestedScroll(parent, layout, target, type)
|
||||||
|
if (toolbarHeight == 0) {
|
||||||
|
toolbarHeight = layout.findChild<Toolbar>()?.height ?: 0
|
||||||
|
}
|
||||||
|
if (lastStartedType == ViewCompat.TYPE_TOUCH || type == ViewCompat.TYPE_NON_TOUCH) {
|
||||||
|
animateToolbarVisibility(
|
||||||
|
parent,
|
||||||
|
layout,
|
||||||
|
getTopBottomOffsetForScrollingSibling(layout) > -toolbarHeight / 2
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFlingFinished(parent: CoordinatorLayout, layout: AppBarLayout) {
|
||||||
|
super.onFlingFinished(parent, layout)
|
||||||
|
animateToolbarVisibility(
|
||||||
|
parent,
|
||||||
|
layout,
|
||||||
|
getTopBottomOffsetForScrollingSibling(layout) > -toolbarHeight / 2
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getTopBottomOffsetForScrollingSibling(abl: AppBarLayout): Int {
|
||||||
|
return topBottomOffsetForScrollingSibling - abl.marginTop
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun animateToolbarVisibility(
|
||||||
|
coordinatorLayout: CoordinatorLayout,
|
||||||
|
child: AppBarLayout,
|
||||||
|
isVisible: Boolean
|
||||||
|
) {
|
||||||
|
val current = getTopBottomOffsetForScrollingSibling(child)
|
||||||
|
val target = if (isVisible) 0 else -toolbarHeight
|
||||||
|
if (current == target) return
|
||||||
|
|
||||||
|
offsetAnimator?.cancel()
|
||||||
|
offsetAnimator = ValueAnimator().apply {
|
||||||
|
interpolator = DecelerateInterpolator()
|
||||||
|
duration = (150 * child.context.animatorDurationScale).roundToLong()
|
||||||
|
addUpdateListener {
|
||||||
|
setHeaderTopBottomOffset(coordinatorLayout, child, it.animatedValue as Int)
|
||||||
|
}
|
||||||
|
doOnEnd {
|
||||||
|
if ((child as? ElevationAppBarLayout)?.isTransparentWhenNotLifted == true) {
|
||||||
|
child.isLifted = !isVisible
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setIntValues(current, target)
|
||||||
|
start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,42 +1,57 @@
|
|||||||
package eu.kanade.tachiyomi
|
package eu.kanade.tachiyomi
|
||||||
|
|
||||||
|
import android.app.ActivityManager
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.res.Configuration
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.lifecycle.Lifecycle
|
import android.webkit.WebView
|
||||||
import androidx.lifecycle.LifecycleObserver
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
import androidx.lifecycle.OnLifecycleEvent
|
import androidx.core.app.NotificationManagerCompat
|
||||||
|
import androidx.core.content.getSystemService
|
||||||
|
import androidx.lifecycle.DefaultLifecycleObserver
|
||||||
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import androidx.lifecycle.ProcessLifecycleOwner
|
import androidx.lifecycle.ProcessLifecycleOwner
|
||||||
import androidx.multidex.MultiDex
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import coil.ImageLoader
|
||||||
|
import coil.ImageLoaderFactory
|
||||||
|
import coil.decode.GifDecoder
|
||||||
|
import coil.decode.ImageDecoderDecoder
|
||||||
|
import eu.kanade.tachiyomi.data.coil.ByteBufferFetcher
|
||||||
|
import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher
|
||||||
|
import eu.kanade.tachiyomi.data.coil.TachiyomiImageDecoder
|
||||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
|
import eu.kanade.tachiyomi.data.preference.PreferenceValues
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
|
import eu.kanade.tachiyomi.data.preference.asImmediateFlow
|
||||||
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate
|
import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate
|
||||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil
|
||||||
import org.acra.ACRA
|
import eu.kanade.tachiyomi.util.system.animatorDurationScale
|
||||||
import org.acra.annotation.AcraCore
|
import eu.kanade.tachiyomi.util.system.notification
|
||||||
import org.acra.annotation.AcraHttpSender
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import org.acra.config.httpSender
|
||||||
|
import org.acra.ktx.initAcra
|
||||||
import org.acra.sender.HttpSender
|
import org.acra.sender.HttpSender
|
||||||
import org.conscrypt.Conscrypt
|
import org.conscrypt.Conscrypt
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.security.Security
|
import java.security.Security
|
||||||
|
|
||||||
@AcraCore(
|
open class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
|
||||||
buildConfigClass = BuildConfig::class,
|
|
||||||
excludeMatchingSharedPreferencesKeys = [".*username.*", ".*password.*", ".*token.*"]
|
|
||||||
)
|
|
||||||
@AcraHttpSender(
|
|
||||||
uri = BuildConfig.ACRA_URI,
|
|
||||||
httpMethod = HttpSender.Method.PUT
|
|
||||||
)
|
|
||||||
open class App : Application(), LifecycleObserver {
|
|
||||||
|
|
||||||
private val preferences: PreferencesHelper by injectLazy()
|
private val preferences: PreferencesHelper by injectLazy()
|
||||||
|
|
||||||
|
private val disableIncognitoReceiver = DisableIncognitoReceiver()
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super<Application>.onCreate()
|
||||||
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
|
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
|
||||||
|
|
||||||
// TLS 1.3 support for Android < 10
|
// TLS 1.3 support for Android < 10
|
||||||
@ -44,41 +59,122 @@ open class App : Application(), LifecycleObserver {
|
|||||||
Security.insertProviderAt(Conscrypt.newProvider(), 1)
|
Security.insertProviderAt(Conscrypt.newProvider(), 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Avoid potential crashes
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
|
val process = getProcessName()
|
||||||
|
if (packageName != process) WebView.setDataDirectorySuffix(process)
|
||||||
|
}
|
||||||
|
|
||||||
Injekt.importModule(AppModule(this))
|
Injekt.importModule(AppModule(this))
|
||||||
|
|
||||||
setupAcra()
|
setupAcra()
|
||||||
setupNotificationChannels()
|
setupNotificationChannels()
|
||||||
|
|
||||||
LocaleHelper.updateConfiguration(this, resources.configuration)
|
|
||||||
|
|
||||||
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
|
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
|
||||||
|
|
||||||
|
// Show notification to disable Incognito Mode when it's enabled
|
||||||
|
preferences.incognitoMode().asFlow()
|
||||||
|
.onEach { enabled ->
|
||||||
|
val notificationManager = NotificationManagerCompat.from(this)
|
||||||
|
if (enabled) {
|
||||||
|
disableIncognitoReceiver.register()
|
||||||
|
val notification = notification(Notifications.CHANNEL_INCOGNITO_MODE) {
|
||||||
|
setContentTitle(getString(R.string.pref_incognito_mode))
|
||||||
|
setContentText(getString(R.string.notification_incognito_text))
|
||||||
|
setSmallIcon(R.drawable.ic_glasses_24dp)
|
||||||
|
setOngoing(true)
|
||||||
|
|
||||||
|
val pendingIntent = PendingIntent.getBroadcast(
|
||||||
|
this@App,
|
||||||
|
0,
|
||||||
|
Intent(ACTION_DISABLE_INCOGNITO_MODE),
|
||||||
|
PendingIntent.FLAG_ONE_SHOT
|
||||||
|
)
|
||||||
|
setContentIntent(pendingIntent)
|
||||||
|
}
|
||||||
|
notificationManager.notify(Notifications.ID_INCOGNITO_MODE, notification)
|
||||||
|
} else {
|
||||||
|
disableIncognitoReceiver.unregister()
|
||||||
|
notificationManager.cancel(Notifications.ID_INCOGNITO_MODE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.launchIn(ProcessLifecycleOwner.get().lifecycleScope)
|
||||||
|
|
||||||
|
preferences.themeMode()
|
||||||
|
.asImmediateFlow {
|
||||||
|
AppCompatDelegate.setDefaultNightMode(
|
||||||
|
when (it) {
|
||||||
|
PreferenceValues.ThemeMode.light -> AppCompatDelegate.MODE_NIGHT_NO
|
||||||
|
PreferenceValues.ThemeMode.dark -> AppCompatDelegate.MODE_NIGHT_YES
|
||||||
|
PreferenceValues.ThemeMode.system -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}.launchIn(ProcessLifecycleOwner.get().lifecycleScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun attachBaseContext(base: Context) {
|
override fun newImageLoader(): ImageLoader {
|
||||||
super.attachBaseContext(base)
|
return ImageLoader.Builder(this).apply {
|
||||||
MultiDex.install(this)
|
componentRegistry {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
|
add(ImageDecoderDecoder(this@App))
|
||||||
|
} else {
|
||||||
|
add(GifDecoder())
|
||||||
|
}
|
||||||
|
add(TachiyomiImageDecoder(this@App.resources))
|
||||||
|
add(ByteBufferFetcher())
|
||||||
|
add(MangaCoverFetcher())
|
||||||
|
}
|
||||||
|
okHttpClient(Injekt.get<NetworkHelper>().coilClient)
|
||||||
|
crossfade((300 * this@App.animatorDurationScale).toInt())
|
||||||
|
allowRgb565(getSystemService<ActivityManager>()!!.isLowRamDevice)
|
||||||
|
}.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
override fun onStop(owner: LifecycleOwner) {
|
||||||
super.onConfigurationChanged(newConfig)
|
if (!AuthenticatorUtil.isAuthenticating && preferences.lockAppAfter().get() >= 0) {
|
||||||
LocaleHelper.updateConfiguration(this, newConfig, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
|
|
||||||
@Suppress("unused")
|
|
||||||
fun onAppBackgrounded() {
|
|
||||||
if (preferences.lockAppAfter().get() >= 0) {
|
|
||||||
SecureActivityDelegate.locked = true
|
SecureActivityDelegate.locked = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected open fun setupAcra() {
|
protected open fun setupAcra() {
|
||||||
if (BuildConfig.FLAVOR != "dev") {
|
if (BuildConfig.FLAVOR != "dev") {
|
||||||
ACRA.init(this)
|
initAcra {
|
||||||
|
buildConfigClass = BuildConfig::class.java
|
||||||
|
excludeMatchingSharedPreferencesKeys = arrayOf(".*username.*", ".*password.*", ".*token.*")
|
||||||
|
|
||||||
|
httpSender {
|
||||||
|
uri = BuildConfig.ACRA_URI
|
||||||
|
httpMethod = HttpSender.Method.PUT
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected open fun setupNotificationChannels() {
|
protected open fun setupNotificationChannels() {
|
||||||
Notifications.createChannels(this)
|
Notifications.createChannels(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private inner class DisableIncognitoReceiver : BroadcastReceiver() {
|
||||||
|
private var registered = false
|
||||||
|
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
preferences.incognitoMode().set(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun register() {
|
||||||
|
if (!registered) {
|
||||||
|
registerReceiver(this, IntentFilter(ACTION_DISABLE_INCOGNITO_MODE))
|
||||||
|
registered = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun unregister() {
|
||||||
|
if (registered) {
|
||||||
|
unregisterReceiver(this)
|
||||||
|
registered = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private const val ACTION_DISABLE_INCOGNITO_MODE = "tachi.action.DISABLE_INCOGNITO_MODE"
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
package eu.kanade.tachiyomi
|
package eu.kanade.tachiyomi
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.os.Handler
|
import androidx.core.content.ContextCompat
|
||||||
import com.google.gson.Gson
|
|
||||||
import eu.kanade.tachiyomi.data.cache.ChapterCache
|
import eu.kanade.tachiyomi.data.cache.ChapterCache
|
||||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
|
import eu.kanade.tachiyomi.data.track.job.DelayedTrackingStore
|
||||||
import eu.kanade.tachiyomi.extension.ExtensionManager
|
import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
@ -24,6 +24,8 @@ class AppModule(val app: Application) : InjektModule {
|
|||||||
override fun InjektRegistrar.registerInjectables() {
|
override fun InjektRegistrar.registerInjectables() {
|
||||||
addSingleton(app)
|
addSingleton(app)
|
||||||
|
|
||||||
|
addSingletonFactory { Json { ignoreUnknownKeys = true } }
|
||||||
|
|
||||||
addSingletonFactory { PreferencesHelper(app) }
|
addSingletonFactory { PreferencesHelper(app) }
|
||||||
|
|
||||||
addSingletonFactory { DatabaseHelper(app) }
|
addSingletonFactory { DatabaseHelper(app) }
|
||||||
@ -42,12 +44,10 @@ class AppModule(val app: Application) : InjektModule {
|
|||||||
|
|
||||||
addSingletonFactory { TrackManager(app) }
|
addSingletonFactory { TrackManager(app) }
|
||||||
|
|
||||||
addSingletonFactory { Gson() }
|
addSingletonFactory { DelayedTrackingStore(app) }
|
||||||
|
|
||||||
addSingletonFactory { Json { ignoreUnknownKeys = true } }
|
|
||||||
|
|
||||||
// Asynchronously init expensive components for a faster cold start
|
// Asynchronously init expensive components for a faster cold start
|
||||||
Handler().post {
|
ContextCompat.getMainExecutor(app).execute {
|
||||||
get<PreferencesHelper>()
|
get<PreferencesHelper>()
|
||||||
|
|
||||||
get<NetworkHelper>()
|
get<NetworkHelper>()
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi
|
package eu.kanade.tachiyomi
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreatorJob
|
import eu.kanade.tachiyomi.data.backup.BackupCreatorJob
|
||||||
@ -9,7 +10,11 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
|||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
import eu.kanade.tachiyomi.data.updater.UpdaterJob
|
import eu.kanade.tachiyomi.data.updater.UpdaterJob
|
||||||
import eu.kanade.tachiyomi.extension.ExtensionUpdateJob
|
import eu.kanade.tachiyomi.extension.ExtensionUpdateJob
|
||||||
|
import eu.kanade.tachiyomi.network.PREF_DOH_CLOUDFLARE
|
||||||
import eu.kanade.tachiyomi.ui.library.LibrarySort
|
import eu.kanade.tachiyomi.ui.library.LibrarySort
|
||||||
|
import eu.kanade.tachiyomi.ui.library.setting.SortDirectionSetting
|
||||||
|
import eu.kanade.tachiyomi.ui.library.setting.SortModeSetting
|
||||||
|
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
import eu.kanade.tachiyomi.widget.ExtendedNavigationView
|
import eu.kanade.tachiyomi.widget.ExtendedNavigationView
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
@ -27,23 +32,20 @@ object Migrations {
|
|||||||
fun upgrade(preferences: PreferencesHelper): Boolean {
|
fun upgrade(preferences: PreferencesHelper): Boolean {
|
||||||
val context = preferences.context
|
val context = preferences.context
|
||||||
|
|
||||||
// Cancel app updater job for debug builds that don't include it
|
|
||||||
if (BuildConfig.DEBUG && !BuildConfig.INCLUDE_UPDATER) {
|
|
||||||
UpdaterJob.cancelTask(context)
|
|
||||||
}
|
|
||||||
|
|
||||||
val oldVersion = preferences.lastVersionCode().get()
|
val oldVersion = preferences.lastVersionCode().get()
|
||||||
if (oldVersion < BuildConfig.VERSION_CODE) {
|
if (oldVersion < BuildConfig.VERSION_CODE) {
|
||||||
preferences.lastVersionCode().set(BuildConfig.VERSION_CODE)
|
preferences.lastVersionCode().set(BuildConfig.VERSION_CODE)
|
||||||
|
|
||||||
|
// Always set up background tasks to ensure they're running
|
||||||
|
if (BuildConfig.INCLUDE_UPDATER) {
|
||||||
|
UpdaterJob.setupTask(context)
|
||||||
|
}
|
||||||
|
ExtensionUpdateJob.setupTask(context)
|
||||||
|
LibraryUpdateJob.setupTask(context)
|
||||||
|
BackupCreatorJob.setupTask(context)
|
||||||
|
|
||||||
// Fresh install
|
// Fresh install
|
||||||
if (oldVersion == 0) {
|
if (oldVersion == 0) {
|
||||||
// Set up default background tasks
|
|
||||||
if (BuildConfig.INCLUDE_UPDATER) {
|
|
||||||
UpdaterJob.setupTask(context)
|
|
||||||
}
|
|
||||||
ExtensionUpdateJob.setupTask(context)
|
|
||||||
LibraryUpdateJob.setupTask(context)
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -93,9 +95,15 @@ object Migrations {
|
|||||||
}
|
}
|
||||||
if (oldVersion < 44) {
|
if (oldVersion < 44) {
|
||||||
// Reset sorting preference if using removed sort by source
|
// Reset sorting preference if using removed sort by source
|
||||||
|
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
|
||||||
|
val oldSortingMode = prefs.getInt(PreferenceKeys.librarySortingMode, 0)
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
if (preferences.librarySortingMode().get() == LibrarySort.SOURCE) {
|
if (oldSortingMode == LibrarySort.SOURCE) {
|
||||||
preferences.librarySortingMode().set(LibrarySort.ALPHA)
|
prefs.edit {
|
||||||
|
putInt(PreferenceKeys.librarySortingMode, LibrarySort.ALPHA)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (oldVersion < 52) {
|
if (oldVersion < 52) {
|
||||||
@ -127,6 +135,101 @@ object Migrations {
|
|||||||
context.toast(R.string.myanimelist_relogin)
|
context.toast(R.string.myanimelist_relogin)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (oldVersion < 57) {
|
||||||
|
// Migrate DNS over HTTPS setting
|
||||||
|
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
val wasDohEnabled = prefs.getBoolean("enable_doh", false)
|
||||||
|
if (wasDohEnabled) {
|
||||||
|
prefs.edit {
|
||||||
|
putInt(PreferenceKeys.dohProvider, PREF_DOH_CLOUDFLARE)
|
||||||
|
remove("enable_doh")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (oldVersion < 59) {
|
||||||
|
// Reset rotation to Free after replacing Lock
|
||||||
|
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
if (prefs.contains("pref_rotation_type_key")) {
|
||||||
|
prefs.edit {
|
||||||
|
putInt("pref_rotation_type_key", 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable update check for Android 5.x users
|
||||||
|
if (BuildConfig.INCLUDE_UPDATER && Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) {
|
||||||
|
UpdaterJob.cancelTask(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (oldVersion < 60) {
|
||||||
|
// Re-enable update check that was prevously accidentally disabled for M
|
||||||
|
if (BuildConfig.INCLUDE_UPDATER && Build.VERSION.SDK_INT == Build.VERSION_CODES.M) {
|
||||||
|
UpdaterJob.setupTask(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Migrate Rotation and Viewer values to default values for viewer_flags
|
||||||
|
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
val newOrientation = when (prefs.getInt("pref_rotation_type_key", 1)) {
|
||||||
|
1 -> OrientationType.FREE.flagValue
|
||||||
|
2 -> OrientationType.PORTRAIT.flagValue
|
||||||
|
3 -> OrientationType.LANDSCAPE.flagValue
|
||||||
|
4 -> OrientationType.LOCKED_PORTRAIT.flagValue
|
||||||
|
5 -> OrientationType.LOCKED_LANDSCAPE.flagValue
|
||||||
|
else -> OrientationType.FREE.flagValue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reading mode flag and prefValue is the same value
|
||||||
|
val newReadingMode = prefs.getInt("pref_default_viewer_key", 1)
|
||||||
|
|
||||||
|
prefs.edit {
|
||||||
|
putInt("pref_default_orientation_type_key", newOrientation)
|
||||||
|
remove("pref_rotation_type_key")
|
||||||
|
putInt("pref_default_reading_mode_key", newReadingMode)
|
||||||
|
remove("pref_default_viewer_key")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (oldVersion < 61) {
|
||||||
|
// Handle removed every 1 or 2 hour library updates
|
||||||
|
val updateInterval = preferences.libraryUpdateInterval().get()
|
||||||
|
if (updateInterval == 1 || updateInterval == 2) {
|
||||||
|
preferences.libraryUpdateInterval().set(3)
|
||||||
|
LibraryUpdateJob.setupTask(context, 3)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (oldVersion < 64) {
|
||||||
|
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
|
||||||
|
val oldSortingMode = prefs.getInt(PreferenceKeys.librarySortingMode, 0)
|
||||||
|
val oldSortingDirection = prefs.getBoolean(PreferenceKeys.librarySortingDirection, true)
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
val newSortingMode = when (oldSortingMode) {
|
||||||
|
LibrarySort.ALPHA -> SortModeSetting.ALPHABETICAL
|
||||||
|
LibrarySort.LAST_READ -> SortModeSetting.LAST_READ
|
||||||
|
LibrarySort.LAST_CHECKED -> SortModeSetting.LAST_CHECKED
|
||||||
|
LibrarySort.UNREAD -> SortModeSetting.UNREAD
|
||||||
|
LibrarySort.TOTAL -> SortModeSetting.TOTAL_CHAPTERS
|
||||||
|
LibrarySort.LATEST_CHAPTER -> SortModeSetting.LATEST_CHAPTER
|
||||||
|
LibrarySort.CHAPTER_FETCH_DATE -> SortModeSetting.DATE_FETCHED
|
||||||
|
LibrarySort.DATE_ADDED -> SortModeSetting.DATE_ADDED
|
||||||
|
else -> SortModeSetting.ALPHABETICAL
|
||||||
|
}
|
||||||
|
|
||||||
|
val newSortingDirection = when (oldSortingDirection) {
|
||||||
|
true -> SortDirectionSetting.ASCENDING
|
||||||
|
else -> SortDirectionSetting.DESCENDING
|
||||||
|
}
|
||||||
|
|
||||||
|
prefs.edit(commit = true) {
|
||||||
|
remove(PreferenceKeys.librarySortingMode)
|
||||||
|
remove(PreferenceKeys.librarySortingDirection)
|
||||||
|
}
|
||||||
|
|
||||||
|
prefs.edit {
|
||||||
|
putString(PreferenceKeys.librarySortingMode, newSortingMode.name)
|
||||||
|
putString(PreferenceKeys.librarySortingDirection, newSortingDirection.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.annotations
|
package eu.kanade.tachiyomi.annotations
|
||||||
|
|
||||||
|
// TODO: remove this when no longer used in extensions
|
||||||
@Retention(AnnotationRetention.RUNTIME)
|
@Retention(AnnotationRetention.RUNTIME)
|
||||||
@Target(AnnotationTarget.CLASS)
|
@Target(AnnotationTarget.CLASS)
|
||||||
annotation class Nsfw
|
annotation class Nsfw
|
||||||
|
@ -8,7 +8,6 @@ object BackupConst {
|
|||||||
const val EXTRA_URI = "$ID.$NAME.EXTRA_URI"
|
const val EXTRA_URI = "$ID.$NAME.EXTRA_URI"
|
||||||
const val EXTRA_FLAGS = "$ID.$NAME.EXTRA_FLAGS"
|
const val EXTRA_FLAGS = "$ID.$NAME.EXTRA_FLAGS"
|
||||||
const val EXTRA_MODE = "$ID.$NAME.EXTRA_MODE"
|
const val EXTRA_MODE = "$ID.$NAME.EXTRA_MODE"
|
||||||
const val EXTRA_TYPE = "$ID.$NAME.EXTRA_TYPE"
|
|
||||||
|
|
||||||
const val BACKUP_TYPE_LEGACY = 0
|
const val BACKUP_TYPE_LEGACY = 0
|
||||||
const val BACKUP_TYPE_FULL = 1
|
const val BACKUP_TYPE_FULL = 1
|
||||||
|
@ -10,7 +10,6 @@ import androidx.core.content.ContextCompat
|
|||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import com.hippo.unifile.UniFile
|
import com.hippo.unifile.UniFile
|
||||||
import eu.kanade.tachiyomi.data.backup.full.FullBackupManager
|
import eu.kanade.tachiyomi.data.backup.full.FullBackupManager
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.LegacyBackupManager
|
|
||||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
import eu.kanade.tachiyomi.util.system.acquireWakeLock
|
import eu.kanade.tachiyomi.util.system.acquireWakeLock
|
||||||
import eu.kanade.tachiyomi.util.system.isServiceRunning
|
import eu.kanade.tachiyomi.util.system.isServiceRunning
|
||||||
@ -48,12 +47,11 @@ class BackupCreateService : Service() {
|
|||||||
* @param uri path of Uri
|
* @param uri path of Uri
|
||||||
* @param flags determines what to backup
|
* @param flags determines what to backup
|
||||||
*/
|
*/
|
||||||
fun start(context: Context, uri: Uri, flags: Int, type: Int) {
|
fun start(context: Context, uri: Uri, flags: Int) {
|
||||||
if (!isRunning(context)) {
|
if (!isRunning(context)) {
|
||||||
val intent = Intent(context, BackupCreateService::class.java).apply {
|
val intent = Intent(context, BackupCreateService::class.java).apply {
|
||||||
putExtra(BackupConst.EXTRA_URI, uri)
|
putExtra(BackupConst.EXTRA_URI, uri)
|
||||||
putExtra(BackupConst.EXTRA_FLAGS, flags)
|
putExtra(BackupConst.EXTRA_FLAGS, flags)
|
||||||
putExtra(BackupConst.EXTRA_TYPE, type)
|
|
||||||
}
|
}
|
||||||
ContextCompat.startForegroundService(context, intent)
|
ContextCompat.startForegroundService(context, intent)
|
||||||
}
|
}
|
||||||
@ -101,17 +99,11 @@ class BackupCreateService : Service() {
|
|||||||
if (intent == null) return START_NOT_STICKY
|
if (intent == null) return START_NOT_STICKY
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val uri = intent.getParcelableExtra<Uri>(BackupConst.EXTRA_URI)
|
val uri = intent.getParcelableExtra<Uri>(BackupConst.EXTRA_URI)!!
|
||||||
val backupFlags = intent.getIntExtra(BackupConst.EXTRA_FLAGS, 0)
|
val backupFlags = intent.getIntExtra(BackupConst.EXTRA_FLAGS, 0)
|
||||||
val backupType = intent.getIntExtra(BackupConst.EXTRA_TYPE, BackupConst.BACKUP_TYPE_LEGACY)
|
val backupFileUri = FullBackupManager(this).createBackup(uri, backupFlags, false)?.toUri()
|
||||||
val backupManager = when (backupType) {
|
|
||||||
BackupConst.BACKUP_TYPE_FULL -> FullBackupManager(this)
|
|
||||||
else -> LegacyBackupManager(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
val backupFileUri = backupManager.createBackup(uri, backupFlags, false)?.toUri()
|
|
||||||
val unifile = UniFile.fromUri(this, backupFileUri)
|
val unifile = UniFile.fromUri(this, backupFileUri)
|
||||||
notifier.showBackupComplete(unifile, backupType == BackupConst.BACKUP_TYPE_LEGACY)
|
notifier.showBackupComplete(unifile)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
notifier.showBackupError(e.message)
|
notifier.showBackupError(e.message)
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,6 @@ import androidx.work.WorkManager
|
|||||||
import androidx.work.Worker
|
import androidx.work.Worker
|
||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
import eu.kanade.tachiyomi.data.backup.full.FullBackupManager
|
import eu.kanade.tachiyomi.data.backup.full.FullBackupManager
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.LegacyBackupManager
|
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
@ -23,9 +22,6 @@ class BackupCreatorJob(private val context: Context, workerParams: WorkerParamet
|
|||||||
val flags = BackupCreateService.BACKUP_ALL
|
val flags = BackupCreateService.BACKUP_ALL
|
||||||
return try {
|
return try {
|
||||||
FullBackupManager(context).createBackup(uri, flags, true)
|
FullBackupManager(context).createBackup(uri, flags, true)
|
||||||
if (preferences.createLegacyBackup().get()) {
|
|
||||||
LegacyBackupManager(context).createBackup(uri, flags, true)
|
|
||||||
}
|
|
||||||
Result.success()
|
Result.success()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Result.failure()
|
Result.failure()
|
||||||
|
@ -24,6 +24,7 @@ class BackupNotifier(private val context: Context) {
|
|||||||
setSmallIcon(R.drawable.ic_tachi)
|
setSmallIcon(R.drawable.ic_tachi)
|
||||||
setAutoCancel(false)
|
setAutoCancel(false)
|
||||||
setOngoing(true)
|
setOngoing(true)
|
||||||
|
setOnlyAlertOnce(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val completeNotificationBuilder = context.notificationBuilder(Notifications.CHANNEL_BACKUP_RESTORE_COMPLETE) {
|
private val completeNotificationBuilder = context.notificationBuilder(Notifications.CHANNEL_BACKUP_RESTORE_COMPLETE) {
|
||||||
@ -41,7 +42,6 @@ class BackupNotifier(private val context: Context) {
|
|||||||
setContentTitle(context.getString(R.string.creating_backup))
|
setContentTitle(context.getString(R.string.creating_backup))
|
||||||
|
|
||||||
setProgress(0, 0, true)
|
setProgress(0, 0, true)
|
||||||
setOnlyAlertOnce(true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
builder.show(Notifications.ID_BACKUP_PROGRESS)
|
builder.show(Notifications.ID_BACKUP_PROGRESS)
|
||||||
@ -60,7 +60,7 @@ class BackupNotifier(private val context: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showBackupComplete(unifile: UniFile, isLegacyFormat: Boolean) {
|
fun showBackupComplete(unifile: UniFile) {
|
||||||
context.notificationManager.cancel(Notifications.ID_BACKUP_PROGRESS)
|
context.notificationManager.cancel(Notifications.ID_BACKUP_PROGRESS)
|
||||||
|
|
||||||
with(completeNotificationBuilder) {
|
with(completeNotificationBuilder) {
|
||||||
@ -73,7 +73,7 @@ class BackupNotifier(private val context: Context) {
|
|||||||
addAction(
|
addAction(
|
||||||
R.drawable.ic_share_24dp,
|
R.drawable.ic_share_24dp,
|
||||||
context.getString(R.string.action_share),
|
context.getString(R.string.action_share),
|
||||||
NotificationReceiver.shareBackupPendingBroadcast(context, unifile.uri, isLegacyFormat, Notifications.ID_BACKUP_COMPLETE)
|
NotificationReceiver.shareBackupPendingBroadcast(context, unifile.uri, Notifications.ID_BACKUP_COMPLETE)
|
||||||
)
|
)
|
||||||
|
|
||||||
show(Notifications.ID_BACKUP_COMPLETE)
|
show(Notifications.ID_BACKUP_COMPLETE)
|
||||||
@ -141,7 +141,7 @@ class BackupNotifier(private val context: Context) {
|
|||||||
|
|
||||||
addAction(
|
addAction(
|
||||||
R.drawable.ic_folder_24dp,
|
R.drawable.ic_folder_24dp,
|
||||||
context.getString(R.string.action_open_log),
|
context.getString(R.string.action_show_errors),
|
||||||
NotificationReceiver.openErrorLogPendingActivity(context, uri)
|
NotificationReceiver.openErrorLogPendingActivity(context, uri)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -43,12 +43,11 @@ class BackupRestoreService : Service() {
|
|||||||
* @param context context of application
|
* @param context context of application
|
||||||
* @param uri path of Uri
|
* @param uri path of Uri
|
||||||
*/
|
*/
|
||||||
fun start(context: Context, uri: Uri, mode: Int, online: Boolean?) {
|
fun start(context: Context, uri: Uri, mode: Int) {
|
||||||
if (!isRunning(context)) {
|
if (!isRunning(context)) {
|
||||||
val intent = Intent(context, BackupRestoreService::class.java).apply {
|
val intent = Intent(context, BackupRestoreService::class.java).apply {
|
||||||
putExtra(BackupConst.EXTRA_URI, uri)
|
putExtra(BackupConst.EXTRA_URI, uri)
|
||||||
putExtra(BackupConst.EXTRA_MODE, mode)
|
putExtra(BackupConst.EXTRA_MODE, mode)
|
||||||
online?.let { putExtra(BackupConst.EXTRA_TYPE, it) }
|
|
||||||
}
|
}
|
||||||
ContextCompat.startForegroundService(context, intent)
|
ContextCompat.startForegroundService(context, intent)
|
||||||
}
|
}
|
||||||
@ -97,7 +96,7 @@ class BackupRestoreService : Service() {
|
|||||||
|
|
||||||
private fun destroyJob() {
|
private fun destroyJob() {
|
||||||
backupRestore?.job?.cancel()
|
backupRestore?.job?.cancel()
|
||||||
ioScope?.cancel()
|
ioScope.cancel()
|
||||||
if (wakeLock.isHeld) {
|
if (wakeLock.isHeld) {
|
||||||
wakeLock.release()
|
wakeLock.release()
|
||||||
}
|
}
|
||||||
@ -119,13 +118,12 @@ class BackupRestoreService : Service() {
|
|||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
val uri = intent?.getParcelableExtra<Uri>(BackupConst.EXTRA_URI) ?: return START_NOT_STICKY
|
val uri = intent?.getParcelableExtra<Uri>(BackupConst.EXTRA_URI) ?: return START_NOT_STICKY
|
||||||
val mode = intent.getIntExtra(BackupConst.EXTRA_MODE, BackupConst.BACKUP_TYPE_FULL)
|
val mode = intent.getIntExtra(BackupConst.EXTRA_MODE, BackupConst.BACKUP_TYPE_FULL)
|
||||||
val online = intent.getBooleanExtra(BackupConst.EXTRA_TYPE, true)
|
|
||||||
|
|
||||||
// Cancel any previous job if needed.
|
// Cancel any previous job if needed.
|
||||||
backupRestore?.job?.cancel()
|
backupRestore?.job?.cancel()
|
||||||
|
|
||||||
backupRestore = when (mode) {
|
backupRestore = when (mode) {
|
||||||
BackupConst.BACKUP_TYPE_FULL -> FullBackupRestore(this, notifier, online)
|
BackupConst.BACKUP_TYPE_FULL -> FullBackupRestore(this, notifier)
|
||||||
else -> LegacyBackupRestore(this, notifier)
|
else -> LegacyBackupRestore(this, notifier)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,9 +26,6 @@ import eu.kanade.tachiyomi.data.database.models.History
|
|||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaCategory
|
import eu.kanade.tachiyomi.data.database.models.MangaCategory
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
import eu.kanade.tachiyomi.data.database.models.toMangaInfo
|
|
||||||
import eu.kanade.tachiyomi.source.Source
|
|
||||||
import eu.kanade.tachiyomi.source.model.toSManga
|
|
||||||
import kotlinx.serialization.protobuf.ProtoBuf
|
import kotlinx.serialization.protobuf.ProtoBuf
|
||||||
import okio.buffer
|
import okio.buffer
|
||||||
import okio.gzip
|
import okio.gzip
|
||||||
@ -56,6 +53,7 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
|||||||
backup = Backup(
|
backup = Backup(
|
||||||
backupManga(databaseManga, flags),
|
backupManga(databaseManga, flags),
|
||||||
backupCategories(),
|
backupCategories(),
|
||||||
|
emptyList(),
|
||||||
backupExtensionInfo(databaseManga)
|
backupExtensionInfo(databaseManga)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -183,24 +181,13 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
|||||||
/**
|
/**
|
||||||
* Fetches manga information
|
* Fetches manga information
|
||||||
*
|
*
|
||||||
* @param source source of manga
|
|
||||||
* @param manga manga that needs updating
|
* @param manga manga that needs updating
|
||||||
* @return Updated manga info.
|
* @return Updated manga info.
|
||||||
*/
|
*/
|
||||||
suspend fun restoreMangaFetch(source: Source?, manga: Manga, online: Boolean): Manga {
|
fun restoreManga(manga: Manga): Manga {
|
||||||
return if (online && source != null) {
|
return manga.also {
|
||||||
val networkManga = source.getMangaDetails(manga.toMangaInfo())
|
it.initialized = it.description != null
|
||||||
manga.also {
|
it.id = insertManga(it)
|
||||||
it.copyFrom(networkManga.toSManga())
|
|
||||||
it.favorite = manga.favorite
|
|
||||||
it.initialized = true
|
|
||||||
it.id = insertManga(manga)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
manga.also {
|
|
||||||
it.initialized = it.description != null
|
|
||||||
it.id = insertManga(it)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -309,29 +296,26 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
|||||||
val trackToUpdate = mutableListOf<Track>()
|
val trackToUpdate = mutableListOf<Track>()
|
||||||
|
|
||||||
tracks.forEach { track ->
|
tracks.forEach { track ->
|
||||||
val service = trackManager.getService(track.sync_id)
|
var isInDatabase = false
|
||||||
if (service != null && service.isLogged) {
|
for (dbTrack in dbTracks) {
|
||||||
var isInDatabase = false
|
if (track.sync_id == dbTrack.sync_id) {
|
||||||
for (dbTrack in dbTracks) {
|
// The sync is already in the db, only update its fields
|
||||||
if (track.sync_id == dbTrack.sync_id) {
|
if (track.media_id != dbTrack.media_id) {
|
||||||
// The sync is already in the db, only update its fields
|
dbTrack.media_id = track.media_id
|
||||||
if (track.media_id != dbTrack.media_id) {
|
|
||||||
dbTrack.media_id = track.media_id
|
|
||||||
}
|
|
||||||
if (track.library_id != dbTrack.library_id) {
|
|
||||||
dbTrack.library_id = track.library_id
|
|
||||||
}
|
|
||||||
dbTrack.last_chapter_read = max(dbTrack.last_chapter_read, track.last_chapter_read)
|
|
||||||
isInDatabase = true
|
|
||||||
trackToUpdate.add(dbTrack)
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
if (track.library_id != dbTrack.library_id) {
|
||||||
|
dbTrack.library_id = track.library_id
|
||||||
|
}
|
||||||
|
dbTrack.last_chapter_read = max(dbTrack.last_chapter_read, track.last_chapter_read)
|
||||||
|
isInDatabase = true
|
||||||
|
trackToUpdate.add(dbTrack)
|
||||||
|
break
|
||||||
}
|
}
|
||||||
if (!isInDatabase) {
|
}
|
||||||
// Insert new sync. Let the db assign the id
|
if (!isInDatabase) {
|
||||||
track.id = null
|
// Insert new sync. Let the db assign the id
|
||||||
trackToUpdate.add(track)
|
track.id = null
|
||||||
}
|
trackToUpdate.add(track)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Update database
|
// Update database
|
||||||
@ -340,47 +324,7 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
internal fun restoreChaptersForManga(manga: Manga, chapters: List<Chapter>) {
|
||||||
* Restore the chapters for manga if chapters already in database
|
|
||||||
*
|
|
||||||
* @param manga manga of chapters
|
|
||||||
* @param chapters list containing chapters that get restored
|
|
||||||
* @return boolean answering if chapter fetch is not needed
|
|
||||||
*/
|
|
||||||
internal fun restoreChaptersForManga(manga: Manga, chapters: List<Chapter>): Boolean {
|
|
||||||
val dbChapters = databaseHelper.getChapters(manga).executeAsBlocking()
|
|
||||||
|
|
||||||
// Return if fetch is needed
|
|
||||||
if (dbChapters.isEmpty() || dbChapters.size < chapters.size) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
chapters.forEach { chapter ->
|
|
||||||
val dbChapter = dbChapters.find { it.url == chapter.url }
|
|
||||||
if (dbChapter != null) {
|
|
||||||
chapter.id = dbChapter.id
|
|
||||||
chapter.copyFrom(dbChapter)
|
|
||||||
if (dbChapter.read && !chapter.read) {
|
|
||||||
chapter.read = dbChapter.read
|
|
||||||
chapter.last_page_read = dbChapter.last_page_read
|
|
||||||
} else if (chapter.last_page_read == 0 && dbChapter.last_page_read != 0) {
|
|
||||||
chapter.last_page_read = dbChapter.last_page_read
|
|
||||||
}
|
|
||||||
if (!chapter.bookmark && dbChapter.bookmark) {
|
|
||||||
chapter.bookmark = dbChapter.bookmark
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
chapter.manga_id = manga.id
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter the chapters that couldn't be found.
|
|
||||||
updateChapters(chapters.filter { it.id != null })
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
internal fun restoreChaptersForMangaOffline(manga: Manga, chapters: List<Chapter>) {
|
|
||||||
val dbChapters = databaseHelper.getChapters(manga).executeAsBlocking()
|
val dbChapters = databaseHelper.getChapters(manga).executeAsBlocking()
|
||||||
|
|
||||||
chapters.forEach { chapter ->
|
chapters.forEach { chapter ->
|
||||||
|
@ -9,16 +9,16 @@ import eu.kanade.tachiyomi.data.backup.full.models.BackupCategory
|
|||||||
import eu.kanade.tachiyomi.data.backup.full.models.BackupHistory
|
import eu.kanade.tachiyomi.data.backup.full.models.BackupHistory
|
||||||
import eu.kanade.tachiyomi.data.backup.full.models.BackupManga
|
import eu.kanade.tachiyomi.data.backup.full.models.BackupManga
|
||||||
import eu.kanade.tachiyomi.data.backup.full.models.BackupSerializer
|
import eu.kanade.tachiyomi.data.backup.full.models.BackupSerializer
|
||||||
|
import eu.kanade.tachiyomi.data.backup.full.models.BackupSource
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
import eu.kanade.tachiyomi.source.Source
|
|
||||||
import okio.buffer
|
import okio.buffer
|
||||||
import okio.gzip
|
import okio.gzip
|
||||||
import okio.source
|
import okio.source
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
|
||||||
class FullBackupRestore(context: Context, notifier: BackupNotifier, private val online: Boolean) : AbstractBackupRestore<FullBackupManager>(context, notifier) {
|
class FullBackupRestore(context: Context, notifier: BackupNotifier) : AbstractBackupRestore<FullBackupManager>(context, notifier) {
|
||||||
|
|
||||||
override suspend fun performRestore(uri: Uri): Boolean {
|
override suspend fun performRestore(uri: Uri): Boolean {
|
||||||
backupManager = FullBackupManager(context)
|
backupManager = FullBackupManager(context)
|
||||||
@ -34,7 +34,8 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier, private val
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Store source mapping for error messages
|
// Store source mapping for error messages
|
||||||
sourceMapping = backup.backupSources.map { it.sourceId to it.name }.toMap()
|
var backupMaps = backup.backupBrokenSources.map { BackupSource(it.name, it.sourceId) } + backup.backupSources
|
||||||
|
sourceMapping = backupMaps.map { it.sourceId to it.name }.toMap()
|
||||||
|
|
||||||
// Restore individual manga
|
// Restore individual manga
|
||||||
backup.backupManga.forEach {
|
backup.backupManga.forEach {
|
||||||
@ -42,9 +43,11 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier, private val
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
restoreManga(it, backup.backupCategories, online)
|
restoreManga(it, backup.backupCategories)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: optionally trigger online library + tracker update
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -57,23 +60,17 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier, private val
|
|||||||
showRestoreProgress(restoreProgress, restoreAmount, context.getString(R.string.categories))
|
showRestoreProgress(restoreProgress, restoreAmount, context.getString(R.string.categories))
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun restoreManga(backupManga: BackupManga, backupCategories: List<BackupCategory>, online: Boolean) {
|
private fun restoreManga(backupManga: BackupManga, backupCategories: List<BackupCategory>) {
|
||||||
val manga = backupManga.getMangaImpl()
|
val manga = backupManga.getMangaImpl()
|
||||||
val chapters = backupManga.getChaptersImpl()
|
val chapters = backupManga.getChaptersImpl()
|
||||||
val categories = backupManga.categories
|
val categories = backupManga.categories
|
||||||
val history = backupManga.history
|
val history = backupManga.brokenHistory.map { BackupHistory(it.url, it.lastRead) } + backupManga.history
|
||||||
val tracks = backupManga.getTrackingImpl()
|
val tracks = backupManga.getTrackingImpl()
|
||||||
|
|
||||||
val source = backupManager.sourceManager.get(manga.source)
|
|
||||||
val sourceName = sourceMapping[manga.source] ?: manga.source.toString()
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (source != null || !online) {
|
restoreMangaData(manga, chapters, categories, history, tracks, backupCategories)
|
||||||
restoreMangaData(manga, source, chapters, categories, history, tracks, backupCategories, online)
|
|
||||||
} else {
|
|
||||||
errors.add(Date() to "${manga.title} [$sourceName]: ${context.getString(R.string.source_not_found_name, sourceName)}")
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
val sourceName = sourceMapping[manga.source] ?: manga.source.toString()
|
||||||
errors.add(Date() to "${manga.title} [$sourceName]: ${e.message}")
|
errors.add(Date() to "${manga.title} [$sourceName]: ${e.message}")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -85,33 +82,30 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier, private val
|
|||||||
* Returns a manga restore observable
|
* Returns a manga restore observable
|
||||||
*
|
*
|
||||||
* @param manga manga data from json
|
* @param manga manga data from json
|
||||||
* @param source source to get manga data from
|
|
||||||
* @param chapters chapters data from json
|
* @param chapters chapters data from json
|
||||||
* @param categories categories data from json
|
* @param categories categories data from json
|
||||||
* @param history history data from json
|
* @param history history data from json
|
||||||
* @param tracks tracking data from json
|
* @param tracks tracking data from json
|
||||||
*/
|
*/
|
||||||
private suspend fun restoreMangaData(
|
private fun restoreMangaData(
|
||||||
manga: Manga,
|
manga: Manga,
|
||||||
source: Source?,
|
|
||||||
chapters: List<Chapter>,
|
chapters: List<Chapter>,
|
||||||
categories: List<Int>,
|
categories: List<Int>,
|
||||||
history: List<BackupHistory>,
|
history: List<BackupHistory>,
|
||||||
tracks: List<Track>,
|
tracks: List<Track>,
|
||||||
backupCategories: List<BackupCategory>,
|
backupCategories: List<BackupCategory>
|
||||||
online: Boolean
|
|
||||||
) {
|
) {
|
||||||
val dbManga = backupManager.getMangaFromDatabase(manga)
|
|
||||||
|
|
||||||
db.inTransaction {
|
db.inTransaction {
|
||||||
|
val dbManga = backupManager.getMangaFromDatabase(manga)
|
||||||
if (dbManga == null) {
|
if (dbManga == null) {
|
||||||
// Manga not in database
|
// Manga not in database
|
||||||
restoreMangaFetch(source, manga, chapters, categories, history, tracks, backupCategories, online)
|
restoreMangaFetch(manga, chapters, categories, history, tracks, backupCategories)
|
||||||
} else { // Manga in database
|
} else {
|
||||||
|
// Manga in database
|
||||||
// Copy information from manga already in database
|
// Copy information from manga already in database
|
||||||
backupManager.restoreMangaNoFetch(manga, dbManga)
|
backupManager.restoreMangaNoFetch(manga, dbManga)
|
||||||
// Fetch rest of manga information
|
// Fetch rest of manga information
|
||||||
restoreMangaNoFetch(source, manga, chapters, categories, history, tracks, backupCategories, online)
|
restoreMangaNoFetch(manga, chapters, categories, history, tracks, backupCategories)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -123,55 +117,37 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier, private val
|
|||||||
* @param chapters chapters of manga that needs updating
|
* @param chapters chapters of manga that needs updating
|
||||||
* @param categories categories that need updating
|
* @param categories categories that need updating
|
||||||
*/
|
*/
|
||||||
private suspend fun restoreMangaFetch(
|
private fun restoreMangaFetch(
|
||||||
source: Source?,
|
|
||||||
manga: Manga,
|
manga: Manga,
|
||||||
chapters: List<Chapter>,
|
chapters: List<Chapter>,
|
||||||
categories: List<Int>,
|
categories: List<Int>,
|
||||||
history: List<BackupHistory>,
|
history: List<BackupHistory>,
|
||||||
tracks: List<Track>,
|
tracks: List<Track>,
|
||||||
backupCategories: List<BackupCategory>,
|
backupCategories: List<BackupCategory>
|
||||||
online: Boolean
|
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
val fetchedManga = backupManager.restoreMangaFetch(source, manga, online)
|
val fetchedManga = backupManager.restoreManga(manga)
|
||||||
fetchedManga.id ?: return
|
fetchedManga.id ?: return
|
||||||
|
|
||||||
if (online && source != null) {
|
backupManager.restoreChaptersForManga(fetchedManga, chapters)
|
||||||
updateChapters(source, fetchedManga, chapters)
|
|
||||||
} else {
|
|
||||||
backupManager.restoreChaptersForMangaOffline(fetchedManga, chapters)
|
|
||||||
}
|
|
||||||
|
|
||||||
restoreExtraForManga(fetchedManga, categories, history, tracks, backupCategories)
|
restoreExtraForManga(fetchedManga, categories, history, tracks, backupCategories)
|
||||||
|
|
||||||
updateTracking(fetchedManga, tracks)
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
errors.add(Date() to "${manga.title} - ${e.message}")
|
errors.add(Date() to "${manga.title} - ${e.message}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun restoreMangaNoFetch(
|
private fun restoreMangaNoFetch(
|
||||||
source: Source?,
|
|
||||||
backupManga: Manga,
|
backupManga: Manga,
|
||||||
chapters: List<Chapter>,
|
chapters: List<Chapter>,
|
||||||
categories: List<Int>,
|
categories: List<Int>,
|
||||||
history: List<BackupHistory>,
|
history: List<BackupHistory>,
|
||||||
tracks: List<Track>,
|
tracks: List<Track>,
|
||||||
backupCategories: List<BackupCategory>,
|
backupCategories: List<BackupCategory>
|
||||||
online: Boolean
|
|
||||||
) {
|
) {
|
||||||
if (online && source != null) {
|
backupManager.restoreChaptersForManga(backupManga, chapters)
|
||||||
if (!backupManager.restoreChaptersForManga(backupManga, chapters)) {
|
|
||||||
updateChapters(source, backupManga, chapters)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
backupManager.restoreChaptersForMangaOffline(backupManga, chapters)
|
|
||||||
}
|
|
||||||
|
|
||||||
restoreExtraForManga(backupManga, categories, history, tracks, backupCategories)
|
restoreExtraForManga(backupManga, categories, history, tracks, backupCategories)
|
||||||
|
|
||||||
updateTracking(backupManga, tracks)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun restoreExtraForManga(manga: Manga, categories: List<Int>, history: List<BackupHistory>, tracks: List<Track>, backupCategories: List<BackupCategory>) {
|
private fun restoreExtraForManga(manga: Manga, categories: List<Int>, history: List<BackupHistory>, tracks: List<Track>, backupCategories: List<BackupCategory>) {
|
||||||
|
@ -8,5 +8,6 @@ data class Backup(
|
|||||||
@ProtoNumber(1) val backupManga: List<BackupManga>,
|
@ProtoNumber(1) val backupManga: List<BackupManga>,
|
||||||
@ProtoNumber(2) var backupCategories: List<BackupCategory> = emptyList(),
|
@ProtoNumber(2) var backupCategories: List<BackupCategory> = emptyList(),
|
||||||
// Bump by 100 to specify this is a 0.x value
|
// Bump by 100 to specify this is a 0.x value
|
||||||
@ProtoNumber(100) var backupSources: List<BackupSource> = emptyList(),
|
@ProtoNumber(100) var backupBrokenSources: List<BrokenBackupSource> = emptyList(),
|
||||||
|
@ProtoNumber(101) var backupSources: List<BackupSource> = emptyList()
|
||||||
)
|
)
|
||||||
|
@ -4,7 +4,13 @@ import kotlinx.serialization.Serializable
|
|||||||
import kotlinx.serialization.protobuf.ProtoNumber
|
import kotlinx.serialization.protobuf.ProtoNumber
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class BackupHistory(
|
data class BrokenBackupHistory(
|
||||||
@ProtoNumber(0) var url: String,
|
@ProtoNumber(0) var url: String,
|
||||||
@ProtoNumber(1) var lastRead: Long
|
@ProtoNumber(1) var lastRead: Long
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class BackupHistory(
|
||||||
|
@ProtoNumber(1) var url: String,
|
||||||
|
@ProtoNumber(2) var lastRead: Long
|
||||||
|
)
|
||||||
|
@ -25,7 +25,7 @@ data class BackupManga(
|
|||||||
// @ProtoNumber(11) val lastUpdate: Long = 0, 1.x value, not used in 0.x
|
// @ProtoNumber(11) val lastUpdate: Long = 0, 1.x value, not used in 0.x
|
||||||
// @ProtoNumber(12) val lastInit: Long = 0, 1.x value, not used in 0.x
|
// @ProtoNumber(12) val lastInit: Long = 0, 1.x value, not used in 0.x
|
||||||
@ProtoNumber(13) var dateAdded: Long = 0,
|
@ProtoNumber(13) var dateAdded: Long = 0,
|
||||||
@ProtoNumber(14) var viewer: Int = 0,
|
@ProtoNumber(14) var viewer: Int = 0, // Replaced by viewer_flags
|
||||||
// @ProtoNumber(15) val flags: Int = 0, 1.x value, not used in 0.x
|
// @ProtoNumber(15) val flags: Int = 0, 1.x value, not used in 0.x
|
||||||
@ProtoNumber(16) var chapters: List<BackupChapter> = emptyList(),
|
@ProtoNumber(16) var chapters: List<BackupChapter> = emptyList(),
|
||||||
@ProtoNumber(17) var categories: List<Int> = emptyList(),
|
@ProtoNumber(17) var categories: List<Int> = emptyList(),
|
||||||
@ -33,7 +33,9 @@ data class BackupManga(
|
|||||||
// Bump by 100 for values that are not saved/implemented in 1.x but are used in 0.x
|
// Bump by 100 for values that are not saved/implemented in 1.x but are used in 0.x
|
||||||
@ProtoNumber(100) var favorite: Boolean = true,
|
@ProtoNumber(100) var favorite: Boolean = true,
|
||||||
@ProtoNumber(101) var chapterFlags: Int = 0,
|
@ProtoNumber(101) var chapterFlags: Int = 0,
|
||||||
@ProtoNumber(102) var history: List<BackupHistory> = emptyList(),
|
@ProtoNumber(102) var brokenHistory: List<BrokenBackupHistory> = emptyList(),
|
||||||
|
@ProtoNumber(103) var viewer_flags: Int? = null,
|
||||||
|
@ProtoNumber(104) var history: List<BackupHistory> = emptyList()
|
||||||
) {
|
) {
|
||||||
fun getMangaImpl(): MangaImpl {
|
fun getMangaImpl(): MangaImpl {
|
||||||
return MangaImpl().apply {
|
return MangaImpl().apply {
|
||||||
@ -48,7 +50,7 @@ data class BackupManga(
|
|||||||
favorite = this@BackupManga.favorite
|
favorite = this@BackupManga.favorite
|
||||||
source = this@BackupManga.source
|
source = this@BackupManga.source
|
||||||
date_added = this@BackupManga.dateAdded
|
date_added = this@BackupManga.dateAdded
|
||||||
viewer = this@BackupManga.viewer
|
viewer_flags = this@BackupManga.viewer_flags ?: this@BackupManga.viewer
|
||||||
chapter_flags = this@BackupManga.chapterFlags
|
chapter_flags = this@BackupManga.chapterFlags
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -79,7 +81,8 @@ data class BackupManga(
|
|||||||
favorite = manga.favorite,
|
favorite = manga.favorite,
|
||||||
source = manga.source,
|
source = manga.source,
|
||||||
dateAdded = manga.date_added,
|
dateAdded = manga.date_added,
|
||||||
viewer = manga.viewer,
|
viewer = manga.readingModeType,
|
||||||
|
viewer_flags = manga.viewer_flags,
|
||||||
chapterFlags = manga.chapter_flags
|
chapterFlags = manga.chapter_flags
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -5,9 +5,15 @@ import kotlinx.serialization.Serializable
|
|||||||
import kotlinx.serialization.protobuf.ProtoNumber
|
import kotlinx.serialization.protobuf.ProtoNumber
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class BackupSource(
|
data class BrokenBackupSource(
|
||||||
@ProtoNumber(0) var name: String = "",
|
@ProtoNumber(0) var name: String = "",
|
||||||
@ProtoNumber(1) var sourceId: Long
|
@ProtoNumber(1) var sourceId: Long
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class BackupSource(
|
||||||
|
@ProtoNumber(1) var name: String = "",
|
||||||
|
@ProtoNumber(2) var sourceId: Long
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
fun copyFrom(source: Source): BackupSource {
|
fun copyFrom(source: Source): BackupSource {
|
||||||
|
@ -32,8 +32,7 @@ data class BackupTracking(
|
|||||||
media_id = this@BackupTracking.mediaId
|
media_id = this@BackupTracking.mediaId
|
||||||
library_id = this@BackupTracking.libraryId
|
library_id = this@BackupTracking.libraryId
|
||||||
title = this@BackupTracking.title
|
title = this@BackupTracking.title
|
||||||
// convert from float to int because of 1.x types
|
last_chapter_read = this@BackupTracking.lastChapterRead
|
||||||
last_chapter_read = this@BackupTracking.lastChapterRead.toInt()
|
|
||||||
total_chapters = this@BackupTracking.totalChapters
|
total_chapters = this@BackupTracking.totalChapters
|
||||||
score = this@BackupTracking.score
|
score = this@BackupTracking.score
|
||||||
status = this@BackupTracking.status
|
status = this@BackupTracking.status
|
||||||
@ -51,8 +50,7 @@ data class BackupTracking(
|
|||||||
// forced not null so its compatible with 1.x backup system
|
// forced not null so its compatible with 1.x backup system
|
||||||
libraryId = track.library_id!!,
|
libraryId = track.library_id!!,
|
||||||
title = track.title,
|
title = track.title,
|
||||||
// convert to float for 1.x
|
lastChapterRead = track.last_chapter_read,
|
||||||
lastChapterRead = track.last_chapter_read.toFloat(),
|
|
||||||
totalChapters = track.total_chapters,
|
totalChapters = track.total_chapters,
|
||||||
score = track.score,
|
score = track.score,
|
||||||
status = track.status,
|
status = track.status,
|
||||||
|
@ -2,65 +2,52 @@ package eu.kanade.tachiyomi.data.backup.legacy
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import com.github.salomonbrys.kotson.fromJson
|
|
||||||
import com.github.salomonbrys.kotson.registerTypeAdapter
|
|
||||||
import com.github.salomonbrys.kotson.registerTypeHierarchyAdapter
|
|
||||||
import com.github.salomonbrys.kotson.set
|
|
||||||
import com.google.gson.Gson
|
|
||||||
import com.google.gson.GsonBuilder
|
|
||||||
import com.google.gson.JsonArray
|
|
||||||
import com.google.gson.JsonElement
|
|
||||||
import com.google.gson.JsonObject
|
|
||||||
import com.hippo.unifile.UniFile
|
|
||||||
import eu.kanade.tachiyomi.data.backup.AbstractBackupManager
|
import eu.kanade.tachiyomi.data.backup.AbstractBackupManager
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CATEGORY
|
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup.Companion.CURRENT_VERSION
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CATEGORY_MASK
|
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CHAPTER
|
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CHAPTER_MASK
|
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_HISTORY
|
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_HISTORY_MASK
|
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_TRACK
|
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_TRACK_MASK
|
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup
|
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup.CATEGORIES
|
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup.CHAPTERS
|
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup.CURRENT_VERSION
|
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup.EXTENSIONS
|
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup.HISTORY
|
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup.MANGA
|
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup.TRACK
|
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.models.DHistory
|
import eu.kanade.tachiyomi.data.backup.legacy.models.DHistory
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.serializer.CategoryTypeAdapter
|
import eu.kanade.tachiyomi.data.backup.legacy.serializer.CategoryImplTypeSerializer
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.serializer.ChapterTypeAdapter
|
import eu.kanade.tachiyomi.data.backup.legacy.serializer.CategoryTypeSerializer
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.serializer.HistoryTypeAdapter
|
import eu.kanade.tachiyomi.data.backup.legacy.serializer.ChapterImplTypeSerializer
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.serializer.MangaTypeAdapter
|
import eu.kanade.tachiyomi.data.backup.legacy.serializer.ChapterTypeSerializer
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.serializer.TrackTypeAdapter
|
import eu.kanade.tachiyomi.data.backup.legacy.serializer.HistoryTypeSerializer
|
||||||
import eu.kanade.tachiyomi.data.database.models.CategoryImpl
|
import eu.kanade.tachiyomi.data.backup.legacy.serializer.MangaImplTypeSerializer
|
||||||
|
import eu.kanade.tachiyomi.data.backup.legacy.serializer.MangaTypeSerializer
|
||||||
|
import eu.kanade.tachiyomi.data.backup.legacy.serializer.TrackImplTypeSerializer
|
||||||
|
import eu.kanade.tachiyomi.data.backup.legacy.serializer.TrackTypeSerializer
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Category
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
import eu.kanade.tachiyomi.data.database.models.ChapterImpl
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.History
|
import eu.kanade.tachiyomi.data.database.models.History
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaCategory
|
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.Track
|
||||||
import eu.kanade.tachiyomi.data.database.models.TrackImpl
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.toMangaInfo
|
import eu.kanade.tachiyomi.data.database.models.toMangaInfo
|
||||||
import eu.kanade.tachiyomi.source.LocalSource
|
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.source.model.toSManga
|
import eu.kanade.tachiyomi.source.model.toSManga
|
||||||
import timber.log.Timber
|
import kotlinx.serialization.json.Json
|
||||||
|
import kotlinx.serialization.modules.SerializersModule
|
||||||
|
import kotlinx.serialization.modules.contextual
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
|
|
||||||
class LegacyBackupManager(context: Context, version: Int = CURRENT_VERSION) : AbstractBackupManager(context) {
|
class LegacyBackupManager(context: Context, version: Int = CURRENT_VERSION) : AbstractBackupManager(context) {
|
||||||
|
|
||||||
val parser: Gson = when (version) {
|
val parser: Json = when (version) {
|
||||||
2 -> GsonBuilder()
|
2 -> Json {
|
||||||
.registerTypeAdapter<MangaImpl>(MangaTypeAdapter.build())
|
// Forks may have added items to backup
|
||||||
.registerTypeHierarchyAdapter<ChapterImpl>(ChapterTypeAdapter.build())
|
ignoreUnknownKeys = true
|
||||||
.registerTypeAdapter<CategoryImpl>(CategoryTypeAdapter.build())
|
|
||||||
.registerTypeAdapter<DHistory>(HistoryTypeAdapter.build())
|
// Register custom serializers
|
||||||
.registerTypeHierarchyAdapter<TrackImpl>(TrackTypeAdapter.build())
|
serializersModule = SerializersModule {
|
||||||
.create()
|
contextual(MangaTypeSerializer)
|
||||||
|
contextual(MangaImplTypeSerializer)
|
||||||
|
contextual(ChapterTypeSerializer)
|
||||||
|
contextual(ChapterImplTypeSerializer)
|
||||||
|
contextual(CategoryTypeSerializer)
|
||||||
|
contextual(CategoryImplTypeSerializer)
|
||||||
|
contextual(TrackTypeSerializer)
|
||||||
|
contextual(TrackImplTypeSerializer)
|
||||||
|
contextual(HistoryTypeSerializer)
|
||||||
|
}
|
||||||
|
}
|
||||||
else -> throw Exception("Unknown backup version")
|
else -> throw Exception("Unknown backup version")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -70,161 +57,8 @@ class LegacyBackupManager(context: Context, version: Int = CURRENT_VERSION) : Ab
|
|||||||
* @param uri path of Uri
|
* @param uri path of Uri
|
||||||
* @param isJob backup called from job
|
* @param isJob backup called from job
|
||||||
*/
|
*/
|
||||||
override fun createBackup(uri: Uri, flags: Int, isJob: Boolean): String? {
|
override fun createBackup(uri: Uri, flags: Int, isJob: Boolean) =
|
||||||
// Create root object
|
throw IllegalStateException("Legacy backup creation is not supported")
|
||||||
val root = JsonObject()
|
|
||||||
|
|
||||||
// Create manga array
|
|
||||||
val mangaEntries = JsonArray()
|
|
||||||
|
|
||||||
// Create category array
|
|
||||||
val categoryEntries = JsonArray()
|
|
||||||
|
|
||||||
// Create extension ID/name mapping
|
|
||||||
val extensionEntries = JsonArray()
|
|
||||||
|
|
||||||
// Add value's to root
|
|
||||||
root[Backup.VERSION] = CURRENT_VERSION
|
|
||||||
root[Backup.MANGAS] = mangaEntries
|
|
||||||
root[CATEGORIES] = categoryEntries
|
|
||||||
root[EXTENSIONS] = extensionEntries
|
|
||||||
|
|
||||||
databaseHelper.inTransaction {
|
|
||||||
val mangas = getFavoriteManga()
|
|
||||||
|
|
||||||
val extensions: MutableSet<String> = mutableSetOf()
|
|
||||||
|
|
||||||
// Backup library manga and its dependencies
|
|
||||||
mangas.forEach { manga ->
|
|
||||||
mangaEntries.add(backupMangaObject(manga, flags))
|
|
||||||
|
|
||||||
// Maintain set of extensions/sources used (excludes local source)
|
|
||||||
if (manga.source != LocalSource.ID) {
|
|
||||||
sourceManager.get(manga.source)?.let {
|
|
||||||
extensions.add("${manga.source}:${it.name}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Backup categories
|
|
||||||
if ((flags and BACKUP_CATEGORY_MASK) == BACKUP_CATEGORY) {
|
|
||||||
backupCategories(categoryEntries)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Backup extension ID/name mapping
|
|
||||||
backupExtensionInfo(extensionEntries, extensions)
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
val file: UniFile = (
|
|
||||||
if (isJob) {
|
|
||||||
// Get dir of file and create
|
|
||||||
var dir = UniFile.fromUri(context, uri)
|
|
||||||
dir = dir.createDirectory("automatic")
|
|
||||||
|
|
||||||
// Delete older backups
|
|
||||||
val numberOfBackups = numberOfBackups()
|
|
||||||
val backupRegex = Regex("""tachiyomi_\d+-\d+-\d+_\d+-\d+.json""")
|
|
||||||
dir.listFiles { _, filename -> backupRegex.matches(filename) }
|
|
||||||
.orEmpty()
|
|
||||||
.sortedByDescending { it.name }
|
|
||||||
.drop(numberOfBackups - 1)
|
|
||||||
.forEach { it.delete() }
|
|
||||||
|
|
||||||
// Create new file to place backup
|
|
||||||
dir.createFile(Backup.getDefaultFilename())
|
|
||||||
} else {
|
|
||||||
UniFile.fromUri(context, uri)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
?: throw Exception("Couldn't create backup file")
|
|
||||||
|
|
||||||
file.openOutputStream().bufferedWriter().use {
|
|
||||||
parser.toJson(root, it)
|
|
||||||
}
|
|
||||||
return file.uri.toString()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.e(e)
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun backupExtensionInfo(root: JsonArray, extensions: Set<String>) {
|
|
||||||
extensions.sorted().forEach {
|
|
||||||
root.add(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Backup the categories of library
|
|
||||||
*
|
|
||||||
* @param root root of categories json
|
|
||||||
*/
|
|
||||||
internal fun backupCategories(root: JsonArray) {
|
|
||||||
val categories = databaseHelper.getCategories().executeAsBlocking()
|
|
||||||
categories.forEach { root.add(parser.toJsonTree(it)) }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert a manga to Json
|
|
||||||
*
|
|
||||||
* @param manga manga that gets converted
|
|
||||||
* @return [JsonElement] containing manga information
|
|
||||||
*/
|
|
||||||
internal fun backupMangaObject(manga: Manga, options: Int): JsonElement {
|
|
||||||
// Entry for this manga
|
|
||||||
val entry = JsonObject()
|
|
||||||
|
|
||||||
// Backup manga fields
|
|
||||||
entry[MANGA] = parser.toJsonTree(manga)
|
|
||||||
|
|
||||||
// Check if user wants chapter information in backup
|
|
||||||
if (options and BACKUP_CHAPTER_MASK == BACKUP_CHAPTER) {
|
|
||||||
// Backup all the chapters
|
|
||||||
val chapters = databaseHelper.getChapters(manga).executeAsBlocking()
|
|
||||||
if (chapters.isNotEmpty()) {
|
|
||||||
val chaptersJson = parser.toJsonTree(chapters)
|
|
||||||
if (chaptersJson.asJsonArray.size() > 0) {
|
|
||||||
entry[CHAPTERS] = chaptersJson
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if user wants category information in backup
|
|
||||||
if (options and BACKUP_CATEGORY_MASK == BACKUP_CATEGORY) {
|
|
||||||
// Backup categories for this manga
|
|
||||||
val categoriesForManga = databaseHelper.getCategoriesForManga(manga).executeAsBlocking()
|
|
||||||
if (categoriesForManga.isNotEmpty()) {
|
|
||||||
val categoriesNames = categoriesForManga.map { it.name }
|
|
||||||
entry[CATEGORIES] = parser.toJsonTree(categoriesNames)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if user wants track information in backup
|
|
||||||
if (options and BACKUP_TRACK_MASK == BACKUP_TRACK) {
|
|
||||||
val tracks = databaseHelper.getTracks(manga).executeAsBlocking()
|
|
||||||
if (tracks.isNotEmpty()) {
|
|
||||||
entry[TRACK] = parser.toJsonTree(tracks)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if user wants history information in backup
|
|
||||||
if (options and BACKUP_HISTORY_MASK == BACKUP_HISTORY) {
|
|
||||||
val historyForManga = databaseHelper.getHistoryByMangaId(manga.id!!).executeAsBlocking()
|
|
||||||
if (historyForManga.isNotEmpty()) {
|
|
||||||
val historyData = historyForManga.mapNotNull { history ->
|
|
||||||
val url = databaseHelper.getChapter(history.chapter_id).executeAsBlocking()?.url
|
|
||||||
url?.let { DHistory(url, history.last_read) }
|
|
||||||
}
|
|
||||||
val historyJson = parser.toJsonTree(historyData)
|
|
||||||
if (historyJson.asJsonArray.size() > 0) {
|
|
||||||
entry[HISTORY] = historyJson
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return entry
|
|
||||||
}
|
|
||||||
|
|
||||||
fun restoreMangaNoFetch(manga: Manga, dbManga: Manga) {
|
fun restoreMangaNoFetch(manga: Manga, dbManga: Manga) {
|
||||||
manga.id = dbManga.id
|
manga.id = dbManga.id
|
||||||
@ -253,12 +87,11 @@ class LegacyBackupManager(context: Context, version: Int = CURRENT_VERSION) : Ab
|
|||||||
/**
|
/**
|
||||||
* Restore the categories from Json
|
* Restore the categories from Json
|
||||||
*
|
*
|
||||||
* @param jsonCategories array containing categories
|
* @param backupCategories array containing categories
|
||||||
*/
|
*/
|
||||||
internal fun restoreCategories(jsonCategories: JsonArray) {
|
internal fun restoreCategories(backupCategories: List<Category>) {
|
||||||
// Get categories from file and from db
|
// Get categories from file and from db
|
||||||
val dbCategories = databaseHelper.getCategories().executeAsBlocking()
|
val dbCategories = databaseHelper.getCategories().executeAsBlocking()
|
||||||
val backupCategories = parser.fromJson<List<CategoryImpl>>(jsonCategories)
|
|
||||||
|
|
||||||
// Iterate over them
|
// Iterate over them
|
||||||
backupCategories.forEach { category ->
|
backupCategories.forEach { category ->
|
||||||
|
@ -2,88 +2,79 @@ package eu.kanade.tachiyomi.data.backup.legacy
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import com.github.salomonbrys.kotson.fromJson
|
|
||||||
import com.google.gson.JsonArray
|
|
||||||
import com.google.gson.JsonElement
|
|
||||||
import com.google.gson.JsonObject
|
|
||||||
import com.google.gson.JsonParser
|
|
||||||
import com.google.gson.stream.JsonReader
|
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.backup.AbstractBackupRestore
|
import eu.kanade.tachiyomi.data.backup.AbstractBackupRestore
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupNotifier
|
import eu.kanade.tachiyomi.data.backup.BackupNotifier
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup
|
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup.MANGAS
|
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.models.DHistory
|
import eu.kanade.tachiyomi.data.backup.legacy.models.DHistory
|
||||||
|
import eu.kanade.tachiyomi.data.backup.legacy.models.MangaObject
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Category
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
import eu.kanade.tachiyomi.data.database.models.ChapterImpl
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
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.Track
|
||||||
import eu.kanade.tachiyomi.data.database.models.TrackImpl
|
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import kotlinx.serialization.json.JsonObject
|
||||||
|
import kotlinx.serialization.json.decodeFromJsonElement
|
||||||
|
import kotlinx.serialization.json.decodeFromStream
|
||||||
|
import kotlinx.serialization.json.intOrNull
|
||||||
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
|
import okio.source
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
|
||||||
class LegacyBackupRestore(context: Context, notifier: BackupNotifier) : AbstractBackupRestore<LegacyBackupManager>(context, notifier) {
|
class LegacyBackupRestore(context: Context, notifier: BackupNotifier) : AbstractBackupRestore<LegacyBackupManager>(context, notifier) {
|
||||||
|
|
||||||
override suspend fun performRestore(uri: Uri): Boolean {
|
override suspend fun performRestore(uri: Uri): Boolean {
|
||||||
val reader = JsonReader(context.contentResolver.openInputStream(uri)!!.bufferedReader())
|
// Read the json and create a Json Object,
|
||||||
val json = JsonParser.parseReader(reader).asJsonObject
|
// cannot use the backupManager json deserializer one because its not initialized yet
|
||||||
|
val backupObject = Json.decodeFromStream<JsonObject>(
|
||||||
|
context.contentResolver.openInputStream(uri)!!
|
||||||
|
)
|
||||||
|
|
||||||
val version = json.get(Backup.VERSION)?.asInt ?: 1
|
// Get parser version
|
||||||
|
val version = backupObject["version"]?.jsonPrimitive?.intOrNull ?: 1
|
||||||
|
|
||||||
|
// Initialize manager
|
||||||
backupManager = LegacyBackupManager(context, version)
|
backupManager = LegacyBackupManager(context, version)
|
||||||
|
|
||||||
val mangasJson = json.get(MANGAS).asJsonArray
|
// Decode the json object to a Backup object
|
||||||
restoreAmount = mangasJson.size() + 1 // +1 for categories
|
val backup = backupManager.parser.decodeFromJsonElement<Backup>(backupObject)
|
||||||
|
|
||||||
|
restoreAmount = backup.mangas.size + 1 // +1 for categories
|
||||||
|
|
||||||
// Restore categories
|
// Restore categories
|
||||||
json.get(Backup.CATEGORIES)?.let { restoreCategories(it) }
|
backup.categories?.let { restoreCategories(it) }
|
||||||
|
|
||||||
// Store source mapping for error messages
|
// Store source mapping for error messages
|
||||||
sourceMapping = LegacyBackupRestoreValidator.getSourceMapping(json)
|
sourceMapping = LegacyBackupRestoreValidator.getSourceMapping(backup.extensions ?: emptyList())
|
||||||
|
|
||||||
// Restore individual manga
|
// Restore individual manga
|
||||||
mangasJson.forEach {
|
backup.mangas.forEach {
|
||||||
if (job?.isActive != true) {
|
if (job?.isActive != true) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
restoreManga(it.asJsonObject)
|
restoreManga(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun restoreCategories(categoriesJson: JsonElement) {
|
private fun restoreCategories(categoriesJson: List<Category>) {
|
||||||
db.inTransaction {
|
db.inTransaction {
|
||||||
backupManager.restoreCategories(categoriesJson.asJsonArray)
|
backupManager.restoreCategories(categoriesJson)
|
||||||
}
|
}
|
||||||
|
|
||||||
restoreProgress += 1
|
restoreProgress += 1
|
||||||
showRestoreProgress(restoreProgress, restoreAmount, context.getString(R.string.categories))
|
showRestoreProgress(restoreProgress, restoreAmount, context.getString(R.string.categories))
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun restoreManga(mangaJson: JsonObject) {
|
private suspend fun restoreManga(mangaJson: MangaObject) {
|
||||||
val manga = backupManager.parser.fromJson<MangaImpl>(
|
val manga = mangaJson.manga
|
||||||
mangaJson.get(
|
val chapters = mangaJson.chapters ?: emptyList()
|
||||||
Backup.MANGA
|
val categories = mangaJson.categories ?: emptyList()
|
||||||
)
|
val history = mangaJson.history ?: emptyList()
|
||||||
)
|
val tracks = mangaJson.track ?: emptyList()
|
||||||
val chapters = backupManager.parser.fromJson<List<ChapterImpl>>(
|
|
||||||
mangaJson.get(Backup.CHAPTERS)
|
|
||||||
?: JsonArray()
|
|
||||||
)
|
|
||||||
val categories = backupManager.parser.fromJson<List<String>>(
|
|
||||||
mangaJson.get(Backup.CATEGORIES)
|
|
||||||
?: JsonArray()
|
|
||||||
)
|
|
||||||
val history = backupManager.parser.fromJson<List<DHistory>>(
|
|
||||||
mangaJson.get(Backup.HISTORY)
|
|
||||||
?: JsonArray()
|
|
||||||
)
|
|
||||||
val tracks = backupManager.parser.fromJson<List<TrackImpl>>(
|
|
||||||
mangaJson.get(Backup.TRACK)
|
|
||||||
?: JsonArray()
|
|
||||||
)
|
|
||||||
|
|
||||||
val source = backupManager.sourceManager.get(manga.source)
|
val source = backupManager.sourceManager.get(manga.source)
|
||||||
val sourceName = sourceMapping[manga.source] ?: manga.source.toString()
|
val sourceName = sourceMapping[manga.source] ?: manga.source.toString()
|
||||||
|
@ -2,12 +2,10 @@ package eu.kanade.tachiyomi.data.backup.legacy
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
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.R
|
||||||
import eu.kanade.tachiyomi.data.backup.AbstractBackupRestoreValidator
|
import eu.kanade.tachiyomi.data.backup.AbstractBackupRestoreValidator
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup
|
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup
|
||||||
|
import kotlinx.serialization.json.decodeFromStream
|
||||||
|
|
||||||
class LegacyBackupRestoreValidator : AbstractBackupRestoreValidator() {
|
class LegacyBackupRestoreValidator : AbstractBackupRestoreValidator() {
|
||||||
/**
|
/**
|
||||||
@ -17,30 +15,30 @@ class LegacyBackupRestoreValidator : AbstractBackupRestoreValidator() {
|
|||||||
* @return List of missing sources or missing trackers.
|
* @return List of missing sources or missing trackers.
|
||||||
*/
|
*/
|
||||||
override fun validate(context: Context, uri: Uri): Results {
|
override fun validate(context: Context, uri: Uri): Results {
|
||||||
val reader = JsonReader(context.contentResolver.openInputStream(uri)!!.bufferedReader())
|
val backupManager = LegacyBackupManager(context)
|
||||||
val json = JsonParser.parseReader(reader).asJsonObject
|
|
||||||
|
|
||||||
val version = json.get(Backup.VERSION)
|
val backup = backupManager.parser.decodeFromStream<Backup>(
|
||||||
val mangasJson = json.get(Backup.MANGAS)
|
context.contentResolver.openInputStream(uri)!!
|
||||||
if (version == null || mangasJson == null) {
|
)
|
||||||
|
|
||||||
|
if (backup.version == null) {
|
||||||
throw Exception(context.getString(R.string.invalid_backup_file_missing_data))
|
throw Exception(context.getString(R.string.invalid_backup_file_missing_data))
|
||||||
}
|
}
|
||||||
|
|
||||||
val mangas = mangasJson.asJsonArray
|
if (backup.mangas.isEmpty()) {
|
||||||
if (mangas.size() == 0) {
|
|
||||||
throw Exception(context.getString(R.string.invalid_backup_file_missing_manga))
|
throw Exception(context.getString(R.string.invalid_backup_file_missing_manga))
|
||||||
}
|
}
|
||||||
|
|
||||||
val sources = getSourceMapping(json)
|
val sources = getSourceMapping(backup.extensions ?: emptyList())
|
||||||
val missingSources = sources
|
val missingSources = sources
|
||||||
.filter { sourceManager.get(it.key) == null }
|
.filter { sourceManager.get(it.key) == null }
|
||||||
.values
|
.values
|
||||||
.sorted()
|
.sorted()
|
||||||
|
|
||||||
val trackers = mangas
|
val trackers = backup.mangas
|
||||||
.filter { it.asJsonObject.has("track") }
|
.filterNot { it.track.isNullOrEmpty() }
|
||||||
.flatMap { it.asJsonObject["track"].asJsonArray }
|
.flatMap { it.track ?: emptyList() }
|
||||||
.map { it.asJsonObject["s"].asInt }
|
.map { it.sync_id }
|
||||||
.distinct()
|
.distinct()
|
||||||
val missingTrackers = trackers
|
val missingTrackers = trackers
|
||||||
.mapNotNull { trackManager.getService(it) }
|
.mapNotNull { trackManager.getService(it) }
|
||||||
@ -52,12 +50,10 @@ class LegacyBackupRestoreValidator : AbstractBackupRestoreValidator() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun getSourceMapping(json: JsonObject): Map<Long, String> {
|
fun getSourceMapping(extensionsMapping: List<String>): Map<Long, String> {
|
||||||
val extensionsMapping = json.get(Backup.EXTENSIONS) ?: return emptyMap()
|
return extensionsMapping
|
||||||
|
|
||||||
return extensionsMapping.asJsonArray
|
|
||||||
.map {
|
.map {
|
||||||
val items = it.asString.split(":")
|
val items = it.split(":")
|
||||||
items[0].toLong() to items[1]
|
items[0].toLong() to items[1]
|
||||||
}
|
}
|
||||||
.toMap()
|
.toMap()
|
||||||
|
@ -1,25 +1,37 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.legacy.models
|
package eu.kanade.tachiyomi.data.backup.legacy.models
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Category
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
|
import kotlinx.serialization.Contextual
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
/**
|
@Serializable
|
||||||
* Json values
|
data class Backup(
|
||||||
*/
|
val version: Int? = null,
|
||||||
object Backup {
|
var mangas: MutableList<MangaObject> = mutableListOf(),
|
||||||
const val CURRENT_VERSION = 2
|
var categories: List<@Contextual Category>? = null,
|
||||||
const val MANGA = "manga"
|
var extensions: List<String>? = null
|
||||||
const val MANGAS = "mangas"
|
) {
|
||||||
const val TRACK = "track"
|
companion object {
|
||||||
const val CHAPTERS = "chapters"
|
const val CURRENT_VERSION = 2
|
||||||
const val CATEGORIES = "categories"
|
|
||||||
const val EXTENSIONS = "extensions"
|
|
||||||
const val HISTORY = "history"
|
|
||||||
const val VERSION = "version"
|
|
||||||
|
|
||||||
fun getDefaultFilename(): String {
|
fun getDefaultFilename(): String {
|
||||||
val date = SimpleDateFormat("yyyy-MM-dd_HH-mm", Locale.getDefault()).format(Date())
|
val date = SimpleDateFormat("yyyy-MM-dd_HH-mm", Locale.getDefault()).format(Date())
|
||||||
return "tachiyomi_$date.json"
|
return "tachiyomi_$date.json"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class MangaObject(
|
||||||
|
var manga: @Contextual Manga,
|
||||||
|
var chapters: List<@Contextual Chapter>? = null,
|
||||||
|
var categories: List<String>? = null,
|
||||||
|
var track: List<@Contextual Track>? = null,
|
||||||
|
var history: List<@Contextual DHistory>? = null
|
||||||
|
)
|
||||||
|
@ -1,31 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.legacy.serializer
|
|
||||||
|
|
||||||
import com.github.salomonbrys.kotson.typeAdapter
|
|
||||||
import com.google.gson.TypeAdapter
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.CategoryImpl
|
|
||||||
|
|
||||||
/**
|
|
||||||
* JSON Serializer used to write / read [CategoryImpl] to / from json
|
|
||||||
*/
|
|
||||||
object CategoryTypeAdapter {
|
|
||||||
|
|
||||||
fun build(): TypeAdapter<CategoryImpl> {
|
|
||||||
return typeAdapter {
|
|
||||||
write {
|
|
||||||
beginArray()
|
|
||||||
value(it.name)
|
|
||||||
value(it.order)
|
|
||||||
endArray()
|
|
||||||
}
|
|
||||||
|
|
||||||
read {
|
|
||||||
beginArray()
|
|
||||||
val category = CategoryImpl()
|
|
||||||
category.name = nextString()
|
|
||||||
category.order = nextInt()
|
|
||||||
endArray()
|
|
||||||
category
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,49 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.legacy.serializer
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Category
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.CategoryImpl
|
||||||
|
import kotlinx.serialization.KSerializer
|
||||||
|
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||||
|
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
|
||||||
|
import kotlinx.serialization.encoding.Decoder
|
||||||
|
import kotlinx.serialization.encoding.Encoder
|
||||||
|
import kotlinx.serialization.json.JsonDecoder
|
||||||
|
import kotlinx.serialization.json.JsonEncoder
|
||||||
|
import kotlinx.serialization.json.add
|
||||||
|
import kotlinx.serialization.json.buildJsonArray
|
||||||
|
import kotlinx.serialization.json.int
|
||||||
|
import kotlinx.serialization.json.jsonArray
|
||||||
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON Serializer used to write / read [CategoryImpl] to / from json
|
||||||
|
*/
|
||||||
|
open class CategoryBaseSerializer<T : Category> : KSerializer<T> {
|
||||||
|
override val descriptor: SerialDescriptor = buildClassSerialDescriptor("Category")
|
||||||
|
|
||||||
|
override fun serialize(encoder: Encoder, value: T) {
|
||||||
|
encoder as JsonEncoder
|
||||||
|
encoder.encodeJsonElement(
|
||||||
|
buildJsonArray {
|
||||||
|
add(value.name)
|
||||||
|
add(value.order)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
override fun deserialize(decoder: Decoder): T {
|
||||||
|
// make a category impl and cast as T so that the serializer accepts it
|
||||||
|
return CategoryImpl().apply {
|
||||||
|
decoder as JsonDecoder
|
||||||
|
val array = decoder.decodeJsonElement().jsonArray
|
||||||
|
name = array[0].jsonPrimitive.content
|
||||||
|
order = array[1].jsonPrimitive.int
|
||||||
|
} as T
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow for serialization of a category and category impl
|
||||||
|
object CategoryTypeSerializer : CategoryBaseSerializer<Category>()
|
||||||
|
|
||||||
|
object CategoryImplTypeSerializer : CategoryBaseSerializer<CategoryImpl>()
|
@ -1,59 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.legacy.serializer
|
|
||||||
|
|
||||||
import com.github.salomonbrys.kotson.typeAdapter
|
|
||||||
import com.google.gson.TypeAdapter
|
|
||||||
import com.google.gson.stream.JsonToken
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.ChapterImpl
|
|
||||||
|
|
||||||
/**
|
|
||||||
* JSON Serializer used to write / read [ChapterImpl] to / from json
|
|
||||||
*/
|
|
||||||
object ChapterTypeAdapter {
|
|
||||||
|
|
||||||
private const val URL = "u"
|
|
||||||
private const val READ = "r"
|
|
||||||
private const val BOOKMARK = "b"
|
|
||||||
private const val LAST_READ = "l"
|
|
||||||
|
|
||||||
fun build(): TypeAdapter<ChapterImpl> {
|
|
||||||
return typeAdapter {
|
|
||||||
write {
|
|
||||||
if (it.read || it.bookmark || it.last_page_read != 0) {
|
|
||||||
beginObject()
|
|
||||||
name(URL)
|
|
||||||
value(it.url)
|
|
||||||
if (it.read) {
|
|
||||||
name(READ)
|
|
||||||
value(1)
|
|
||||||
}
|
|
||||||
if (it.bookmark) {
|
|
||||||
name(BOOKMARK)
|
|
||||||
value(1)
|
|
||||||
}
|
|
||||||
if (it.last_page_read != 0) {
|
|
||||||
name(LAST_READ)
|
|
||||||
value(it.last_page_read)
|
|
||||||
}
|
|
||||||
endObject()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
read {
|
|
||||||
val chapter = ChapterImpl()
|
|
||||||
beginObject()
|
|
||||||
while (hasNext()) {
|
|
||||||
if (peek() == JsonToken.NAME) {
|
|
||||||
when (nextName()) {
|
|
||||||
URL -> chapter.url = nextString()
|
|
||||||
READ -> chapter.read = nextInt() == 1
|
|
||||||
BOOKMARK -> chapter.bookmark = nextInt() == 1
|
|
||||||
LAST_READ -> chapter.last_page_read = nextInt()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
endObject()
|
|
||||||
chapter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,66 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.legacy.serializer
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.ChapterImpl
|
||||||
|
import kotlinx.serialization.KSerializer
|
||||||
|
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
|
||||||
|
import kotlinx.serialization.encoding.Decoder
|
||||||
|
import kotlinx.serialization.encoding.Encoder
|
||||||
|
import kotlinx.serialization.json.JsonDecoder
|
||||||
|
import kotlinx.serialization.json.JsonEncoder
|
||||||
|
import kotlinx.serialization.json.buildJsonObject
|
||||||
|
import kotlinx.serialization.json.intOrNull
|
||||||
|
import kotlinx.serialization.json.jsonObject
|
||||||
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
|
import kotlinx.serialization.json.put
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON Serializer used to write / read [ChapterImpl] to / from json
|
||||||
|
*/
|
||||||
|
open class ChapterBaseSerializer<T : Chapter> : KSerializer<T> {
|
||||||
|
|
||||||
|
override val descriptor = buildClassSerialDescriptor("Chapter")
|
||||||
|
|
||||||
|
override fun serialize(encoder: Encoder, value: T) {
|
||||||
|
encoder as JsonEncoder
|
||||||
|
encoder.encodeJsonElement(
|
||||||
|
buildJsonObject {
|
||||||
|
put(URL, value.url)
|
||||||
|
if (value.read) {
|
||||||
|
put(READ, 1)
|
||||||
|
}
|
||||||
|
if (value.bookmark) {
|
||||||
|
put(BOOKMARK, 1)
|
||||||
|
}
|
||||||
|
if (value.last_page_read != 0) {
|
||||||
|
put(LAST_READ, value.last_page_read)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
override fun deserialize(decoder: Decoder): T {
|
||||||
|
// make a chapter impl and cast as T so that the serializer accepts it
|
||||||
|
return ChapterImpl().apply {
|
||||||
|
decoder as JsonDecoder
|
||||||
|
val jsonObject = decoder.decodeJsonElement().jsonObject
|
||||||
|
url = jsonObject[URL]!!.jsonPrimitive.content
|
||||||
|
read = jsonObject[READ]?.jsonPrimitive?.intOrNull == 1
|
||||||
|
bookmark = jsonObject[BOOKMARK]?.jsonPrimitive?.intOrNull == 1
|
||||||
|
last_page_read = jsonObject[LAST_READ]?.jsonPrimitive?.intOrNull ?: last_page_read
|
||||||
|
} as T
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val URL = "u"
|
||||||
|
private const val READ = "r"
|
||||||
|
private const val BOOKMARK = "b"
|
||||||
|
private const val LAST_READ = "l"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow for serialization of a chapter and chapter impl
|
||||||
|
object ChapterTypeSerializer : ChapterBaseSerializer<Chapter>()
|
||||||
|
|
||||||
|
object ChapterImplTypeSerializer : ChapterBaseSerializer<ChapterImpl>()
|
@ -1,32 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.legacy.serializer
|
|
||||||
|
|
||||||
import com.github.salomonbrys.kotson.typeAdapter
|
|
||||||
import com.google.gson.TypeAdapter
|
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.models.DHistory
|
|
||||||
|
|
||||||
/**
|
|
||||||
* JSON Serializer used to write / read [DHistory] to / from json
|
|
||||||
*/
|
|
||||||
object HistoryTypeAdapter {
|
|
||||||
|
|
||||||
fun build(): TypeAdapter<DHistory> {
|
|
||||||
return typeAdapter {
|
|
||||||
write {
|
|
||||||
if (it.lastRead != 0L) {
|
|
||||||
beginArray()
|
|
||||||
value(it.url)
|
|
||||||
value(it.lastRead)
|
|
||||||
endArray()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
read {
|
|
||||||
beginArray()
|
|
||||||
val url = nextString()
|
|
||||||
val lastRead = nextLong()
|
|
||||||
endArray()
|
|
||||||
DHistory(url, lastRead)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,41 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.legacy.serializer
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.backup.legacy.models.DHistory
|
||||||
|
import kotlinx.serialization.KSerializer
|
||||||
|
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||||
|
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
|
||||||
|
import kotlinx.serialization.encoding.Decoder
|
||||||
|
import kotlinx.serialization.encoding.Encoder
|
||||||
|
import kotlinx.serialization.json.JsonDecoder
|
||||||
|
import kotlinx.serialization.json.JsonEncoder
|
||||||
|
import kotlinx.serialization.json.add
|
||||||
|
import kotlinx.serialization.json.buildJsonArray
|
||||||
|
import kotlinx.serialization.json.jsonArray
|
||||||
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
|
import kotlinx.serialization.json.long
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON Serializer used to write / read [DHistory] to / from json
|
||||||
|
*/
|
||||||
|
object HistoryTypeSerializer : KSerializer<DHistory> {
|
||||||
|
override val descriptor: SerialDescriptor = buildClassSerialDescriptor("History")
|
||||||
|
|
||||||
|
override fun serialize(encoder: Encoder, value: DHistory) {
|
||||||
|
encoder as JsonEncoder
|
||||||
|
encoder.encodeJsonElement(
|
||||||
|
buildJsonArray {
|
||||||
|
add(value.url)
|
||||||
|
add(value.lastRead)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun deserialize(decoder: Decoder): DHistory {
|
||||||
|
decoder as JsonDecoder
|
||||||
|
val array = decoder.decodeJsonElement().jsonArray
|
||||||
|
return DHistory(
|
||||||
|
url = array[0].jsonPrimitive.content,
|
||||||
|
lastRead = array[1].jsonPrimitive.long
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -1,37 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.legacy.serializer
|
|
||||||
|
|
||||||
import com.github.salomonbrys.kotson.typeAdapter
|
|
||||||
import com.google.gson.TypeAdapter
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaImpl
|
|
||||||
|
|
||||||
/**
|
|
||||||
* JSON Serializer used to write / read [MangaImpl] to / from json
|
|
||||||
*/
|
|
||||||
object MangaTypeAdapter {
|
|
||||||
|
|
||||||
fun build(): TypeAdapter<MangaImpl> {
|
|
||||||
return typeAdapter {
|
|
||||||
write {
|
|
||||||
beginArray()
|
|
||||||
value(it.url)
|
|
||||||
value(it.title)
|
|
||||||
value(it.source)
|
|
||||||
value(it.viewer)
|
|
||||||
value(it.chapter_flags)
|
|
||||||
endArray()
|
|
||||||
}
|
|
||||||
|
|
||||||
read {
|
|
||||||
beginArray()
|
|
||||||
val manga = MangaImpl()
|
|
||||||
manga.url = nextString()
|
|
||||||
manga.title = nextString()
|
|
||||||
manga.source = nextLong()
|
|
||||||
manga.viewer = nextInt()
|
|
||||||
manga.chapter_flags = nextInt()
|
|
||||||
endArray()
|
|
||||||
manga
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,56 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.legacy.serializer
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.MangaImpl
|
||||||
|
import kotlinx.serialization.KSerializer
|
||||||
|
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||||
|
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
|
||||||
|
import kotlinx.serialization.encoding.Decoder
|
||||||
|
import kotlinx.serialization.encoding.Encoder
|
||||||
|
import kotlinx.serialization.json.JsonDecoder
|
||||||
|
import kotlinx.serialization.json.JsonEncoder
|
||||||
|
import kotlinx.serialization.json.add
|
||||||
|
import kotlinx.serialization.json.buildJsonArray
|
||||||
|
import kotlinx.serialization.json.int
|
||||||
|
import kotlinx.serialization.json.jsonArray
|
||||||
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
|
import kotlinx.serialization.json.long
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON Serializer used to write / read [MangaImpl] to / from json
|
||||||
|
*/
|
||||||
|
open class MangaBaseSerializer<T : Manga> : KSerializer<T> {
|
||||||
|
override val descriptor: SerialDescriptor = buildClassSerialDescriptor("Manga")
|
||||||
|
|
||||||
|
override fun serialize(encoder: Encoder, value: T) {
|
||||||
|
encoder as JsonEncoder
|
||||||
|
encoder.encodeJsonElement(
|
||||||
|
buildJsonArray {
|
||||||
|
add(value.url)
|
||||||
|
add(value.title)
|
||||||
|
add(value.source)
|
||||||
|
add(value.viewer_flags)
|
||||||
|
add(value.chapter_flags)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
override fun deserialize(decoder: Decoder): T {
|
||||||
|
// make a manga impl and cast as T so that the serializer accepts it
|
||||||
|
return MangaImpl().apply {
|
||||||
|
decoder as JsonDecoder
|
||||||
|
val array = decoder.decodeJsonElement().jsonArray
|
||||||
|
url = array[0].jsonPrimitive.content
|
||||||
|
title = array[1].jsonPrimitive.content
|
||||||
|
source = array[2].jsonPrimitive.long
|
||||||
|
viewer_flags = array[3].jsonPrimitive.int
|
||||||
|
chapter_flags = array[4].jsonPrimitive.int
|
||||||
|
} as T
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow for serialization of a manga and manga impl
|
||||||
|
object MangaTypeSerializer : MangaBaseSerializer<Manga>()
|
||||||
|
|
||||||
|
object MangaImplTypeSerializer : MangaBaseSerializer<MangaImpl>()
|
@ -1,59 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.legacy.serializer
|
|
||||||
|
|
||||||
import com.github.salomonbrys.kotson.typeAdapter
|
|
||||||
import com.google.gson.TypeAdapter
|
|
||||||
import com.google.gson.stream.JsonToken
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.TrackImpl
|
|
||||||
|
|
||||||
/**
|
|
||||||
* JSON Serializer used to write / read [TrackImpl] to / from json
|
|
||||||
*/
|
|
||||||
object TrackTypeAdapter {
|
|
||||||
|
|
||||||
private const val SYNC = "s"
|
|
||||||
private const val MEDIA = "r"
|
|
||||||
private const val LIBRARY = "ml"
|
|
||||||
private const val TITLE = "t"
|
|
||||||
private const val LAST_READ = "l"
|
|
||||||
private const val TRACKING_URL = "u"
|
|
||||||
|
|
||||||
fun build(): TypeAdapter<TrackImpl> {
|
|
||||||
return typeAdapter {
|
|
||||||
write {
|
|
||||||
beginObject()
|
|
||||||
name(TITLE)
|
|
||||||
value(it.title)
|
|
||||||
name(SYNC)
|
|
||||||
value(it.sync_id)
|
|
||||||
name(MEDIA)
|
|
||||||
value(it.media_id)
|
|
||||||
name(LIBRARY)
|
|
||||||
value(it.library_id)
|
|
||||||
name(LAST_READ)
|
|
||||||
value(it.last_chapter_read)
|
|
||||||
name(TRACKING_URL)
|
|
||||||
value(it.tracking_url)
|
|
||||||
endObject()
|
|
||||||
}
|
|
||||||
|
|
||||||
read {
|
|
||||||
val track = TrackImpl()
|
|
||||||
beginObject()
|
|
||||||
while (hasNext()) {
|
|
||||||
if (peek() == JsonToken.NAME) {
|
|
||||||
when (nextName()) {
|
|
||||||
TITLE -> track.title = nextString()
|
|
||||||
SYNC -> track.sync_id = nextInt()
|
|
||||||
MEDIA -> track.media_id = nextInt()
|
|
||||||
LIBRARY -> track.library_id = nextLong()
|
|
||||||
LAST_READ -> track.last_chapter_read = nextInt()
|
|
||||||
TRACKING_URL -> track.tracking_url = nextString()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
endObject()
|
|
||||||
track
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,68 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.legacy.serializer
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.TrackImpl
|
||||||
|
import kotlinx.serialization.KSerializer
|
||||||
|
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||||
|
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
|
||||||
|
import kotlinx.serialization.encoding.Decoder
|
||||||
|
import kotlinx.serialization.encoding.Encoder
|
||||||
|
import kotlinx.serialization.json.JsonDecoder
|
||||||
|
import kotlinx.serialization.json.JsonEncoder
|
||||||
|
import kotlinx.serialization.json.buildJsonObject
|
||||||
|
import kotlinx.serialization.json.float
|
||||||
|
import kotlinx.serialization.json.int
|
||||||
|
import kotlinx.serialization.json.jsonObject
|
||||||
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
|
import kotlinx.serialization.json.long
|
||||||
|
import kotlinx.serialization.json.put
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON Serializer used to write / read [TrackImpl] to / from json
|
||||||
|
*/
|
||||||
|
open class TrackBaseSerializer<T : Track> : KSerializer<T> {
|
||||||
|
override val descriptor: SerialDescriptor = buildClassSerialDescriptor("Track")
|
||||||
|
|
||||||
|
override fun serialize(encoder: Encoder, value: T) {
|
||||||
|
encoder as JsonEncoder
|
||||||
|
encoder.encodeJsonElement(
|
||||||
|
buildJsonObject {
|
||||||
|
put(TITLE, value.title)
|
||||||
|
put(SYNC, value.sync_id)
|
||||||
|
put(MEDIA, value.media_id)
|
||||||
|
put(LIBRARY, value.library_id)
|
||||||
|
put(LAST_READ, value.last_chapter_read)
|
||||||
|
put(TRACKING_URL, value.tracking_url)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
override fun deserialize(decoder: Decoder): T {
|
||||||
|
// make a track impl and cast as T so that the serializer accepts it
|
||||||
|
return TrackImpl().apply {
|
||||||
|
decoder as JsonDecoder
|
||||||
|
val jsonObject = decoder.decodeJsonElement().jsonObject
|
||||||
|
title = jsonObject[TITLE]!!.jsonPrimitive.content
|
||||||
|
sync_id = jsonObject[SYNC]!!.jsonPrimitive.int
|
||||||
|
media_id = jsonObject[MEDIA]!!.jsonPrimitive.int
|
||||||
|
library_id = jsonObject[LIBRARY]!!.jsonPrimitive.long
|
||||||
|
last_chapter_read = jsonObject[LAST_READ]!!.jsonPrimitive.float
|
||||||
|
tracking_url = jsonObject[TRACKING_URL]!!.jsonPrimitive.content
|
||||||
|
} as T
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val SYNC = "s"
|
||||||
|
private const val MEDIA = "r"
|
||||||
|
private const val LIBRARY = "ml"
|
||||||
|
private const val TITLE = "t"
|
||||||
|
private const val LAST_READ = "l"
|
||||||
|
private const val TRACKING_URL = "u"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow for serialization of a track and track impl
|
||||||
|
object TrackTypeSerializer : TrackBaseSerializer<Track>()
|
||||||
|
|
||||||
|
object TrackImplTypeSerializer : TrackBaseSerializer<TrackImpl>()
|
@ -2,17 +2,17 @@ package eu.kanade.tachiyomi.data.cache
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.text.format.Formatter
|
import android.text.format.Formatter
|
||||||
import com.github.salomonbrys.kotson.fromJson
|
|
||||||
import com.google.gson.Gson
|
|
||||||
import com.jakewharton.disklrucache.DiskLruCache
|
import com.jakewharton.disklrucache.DiskLruCache
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||||
import eu.kanade.tachiyomi.util.storage.saveTo
|
import eu.kanade.tachiyomi.util.storage.saveTo
|
||||||
|
import kotlinx.serialization.decodeFromString
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import okio.buffer
|
import okio.buffer
|
||||||
import okio.sink
|
import okio.sink
|
||||||
import rx.Observable
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
@ -42,8 +42,7 @@ class ChapterCache(private val context: Context) {
|
|||||||
const val PARAMETER_CACHE_SIZE = 100L * 1024 * 1024
|
const val PARAMETER_CACHE_SIZE = 100L * 1024 * 1024
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Google Json class used for parsing JSON files. */
|
private val json: Json by injectLazy()
|
||||||
private val gson: Gson by injectLazy()
|
|
||||||
|
|
||||||
/** Cache class used for cache management. */
|
/** Cache class used for cache management. */
|
||||||
private val diskCache = DiskLruCache.open(
|
private val diskCache = DiskLruCache.open(
|
||||||
@ -56,7 +55,7 @@ class ChapterCache(private val context: Context) {
|
|||||||
/**
|
/**
|
||||||
* Returns directory of cache.
|
* Returns directory of cache.
|
||||||
*/
|
*/
|
||||||
val cacheDir: File
|
private val cacheDir: File
|
||||||
get() = diskCache.directory
|
get() = diskCache.directory
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -71,43 +70,19 @@ class ChapterCache(private val context: Context) {
|
|||||||
val readableSize: String
|
val readableSize: String
|
||||||
get() = Formatter.formatFileSize(context, realSize)
|
get() = Formatter.formatFileSize(context, realSize)
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove file from cache.
|
|
||||||
*
|
|
||||||
* @param file name of file "md5.0".
|
|
||||||
* @return status of deletion for the file.
|
|
||||||
*/
|
|
||||||
fun removeFileFromCache(file: String): Boolean {
|
|
||||||
// Make sure we don't delete the journal file (keeps track of cache).
|
|
||||||
if (file == "journal" || file.startsWith("journal.")) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return try {
|
|
||||||
// Remove the extension from the file to get the key of the cache
|
|
||||||
val key = file.substringBeforeLast(".")
|
|
||||||
// Remove file from cache.
|
|
||||||
diskCache.remove(key)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get page list from cache.
|
* Get page list from cache.
|
||||||
*
|
*
|
||||||
* @param chapter the chapter.
|
* @param chapter the chapter.
|
||||||
* @return an observable of the list of pages.
|
* @return the list of pages.
|
||||||
*/
|
*/
|
||||||
fun getPageListFromCache(chapter: Chapter): Observable<List<Page>> {
|
fun getPageListFromCache(chapter: Chapter): List<Page> {
|
||||||
return Observable.fromCallable {
|
// Get the key for the chapter.
|
||||||
// Get the key for the chapter.
|
val key = DiskUtil.hashKeyForDisk(getKey(chapter))
|
||||||
val key = DiskUtil.hashKeyForDisk(getKey(chapter))
|
|
||||||
|
|
||||||
// Convert JSON string to list of objects. Throws an exception if snapshot is null
|
// Convert JSON string to list of objects. Throws an exception if snapshot is null
|
||||||
diskCache.get(key).use {
|
return diskCache.get(key).use {
|
||||||
gson.fromJson<List<Page>>(it.getString(0))
|
json.decodeFromString(it.getString(0))
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -119,7 +94,7 @@ class ChapterCache(private val context: Context) {
|
|||||||
*/
|
*/
|
||||||
fun putPageListToCache(chapter: Chapter, pages: List<Page>) {
|
fun putPageListToCache(chapter: Chapter, pages: List<Page>) {
|
||||||
// Convert list of pages to json string.
|
// Convert list of pages to json string.
|
||||||
val cachedValue = gson.toJson(pages)
|
val cachedValue = json.encodeToString(pages)
|
||||||
|
|
||||||
// Initialize the editor (edits the values for an entry).
|
// Initialize the editor (edits the values for an entry).
|
||||||
var editor: DiskLruCache.Editor? = null
|
var editor: DiskLruCache.Editor? = null
|
||||||
@ -199,6 +174,38 @@ class ChapterCache(private val context: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun clear(): Int {
|
||||||
|
var deletedFiles = 0
|
||||||
|
cacheDir.listFiles()?.forEach {
|
||||||
|
if (removeFileFromCache(it.name)) {
|
||||||
|
deletedFiles++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return deletedFiles
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove file from cache.
|
||||||
|
*
|
||||||
|
* @param file name of file "md5.0".
|
||||||
|
* @return status of deletion for the file.
|
||||||
|
*/
|
||||||
|
private fun removeFileFromCache(file: String): Boolean {
|
||||||
|
// Make sure we don't delete the journal file (keeps track of cache).
|
||||||
|
if (file == "journal" || file.startsWith("journal.")) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return try {
|
||||||
|
// Remove the extension from the file to get the key of the cache
|
||||||
|
val key = file.substringBeforeLast(".")
|
||||||
|
// Remove file from cache.
|
||||||
|
diskCache.remove(key)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun getKey(chapter: Chapter): String {
|
private fun getKey(chapter: Chapter): String {
|
||||||
return "${chapter.manga_id}${chapter.url}"
|
return "${chapter.manga_id}${chapter.url}"
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package eu.kanade.tachiyomi.data.cache
|
package eu.kanade.tachiyomi.data.cache
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import coil.imageLoader
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@ -99,6 +100,13 @@ class CoverCache(private val context: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear coil's memory cache.
|
||||||
|
*/
|
||||||
|
fun clearMemoryCache() {
|
||||||
|
context.imageLoader.memoryCache.clear()
|
||||||
|
}
|
||||||
|
|
||||||
private fun getCacheDir(dir: String): File {
|
private fun getCacheDir(dir: String): File {
|
||||||
return context.getExternalFilesDir(dir)
|
return context.getExternalFilesDir(dir)
|
||||||
?: File(context.filesDir, dir).also { it.mkdirs() }
|
?: File(context.filesDir, dir).also { it.mkdirs() }
|
||||||
|
@ -0,0 +1,25 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.coil
|
||||||
|
|
||||||
|
import coil.bitmap.BitmapPool
|
||||||
|
import coil.decode.DataSource
|
||||||
|
import coil.decode.Options
|
||||||
|
import coil.fetch.FetchResult
|
||||||
|
import coil.fetch.Fetcher
|
||||||
|
import coil.fetch.SourceResult
|
||||||
|
import coil.size.Size
|
||||||
|
import okio.buffer
|
||||||
|
import okio.source
|
||||||
|
import java.io.ByteArrayInputStream
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
|
||||||
|
class ByteBufferFetcher : Fetcher<ByteBuffer> {
|
||||||
|
override suspend fun fetch(pool: BitmapPool, data: ByteBuffer, size: Size, options: Options): FetchResult {
|
||||||
|
return SourceResult(
|
||||||
|
source = ByteArrayInputStream(data.array()).source().buffer(),
|
||||||
|
mimeType = null,
|
||||||
|
dataSource = DataSource.MEMORY
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun key(data: ByteBuffer): String? = null
|
||||||
|
}
|
@ -0,0 +1,168 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.coil
|
||||||
|
|
||||||
|
import coil.bitmap.BitmapPool
|
||||||
|
import coil.decode.DataSource
|
||||||
|
import coil.decode.Options
|
||||||
|
import coil.fetch.FetchResult
|
||||||
|
import coil.fetch.Fetcher
|
||||||
|
import coil.fetch.SourceResult
|
||||||
|
import coil.network.HttpException
|
||||||
|
import coil.request.get
|
||||||
|
import coil.size.Size
|
||||||
|
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||||
|
import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher.Companion.USE_CUSTOM_COVER
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
|
import eu.kanade.tachiyomi.network.await
|
||||||
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
|
import okhttp3.CacheControl
|
||||||
|
import okhttp3.Call
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
|
import okhttp3.ResponseBody
|
||||||
|
import okio.buffer
|
||||||
|
import okio.sink
|
||||||
|
import okio.source
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Coil component that fetches [Manga] cover while using the cached file in disk when available.
|
||||||
|
*
|
||||||
|
* Available request parameter:
|
||||||
|
* - [USE_CUSTOM_COVER]: Use custom cover if set by user, default is true
|
||||||
|
*/
|
||||||
|
class MangaCoverFetcher : Fetcher<Manga> {
|
||||||
|
private val coverCache: CoverCache by injectLazy()
|
||||||
|
private val sourceManager: SourceManager by injectLazy()
|
||||||
|
private val defaultClient = Injekt.get<NetworkHelper>().coilClient
|
||||||
|
|
||||||
|
override fun key(data: Manga): String? {
|
||||||
|
if (data.thumbnail_url.isNullOrBlank()) return null
|
||||||
|
return data.thumbnail_url!!
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun fetch(pool: BitmapPool, data: Manga, size: Size, options: Options): FetchResult {
|
||||||
|
// Use custom cover if exists
|
||||||
|
val useCustomCover = options.parameters[USE_CUSTOM_COVER] as? Boolean ?: true
|
||||||
|
val customCoverFile = coverCache.getCustomCoverFile(data)
|
||||||
|
if (useCustomCover && customCoverFile.exists()) {
|
||||||
|
return fileLoader(customCoverFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
val cover = data.thumbnail_url
|
||||||
|
return when (getResourceType(cover)) {
|
||||||
|
Type.URL -> httpLoader(data, options)
|
||||||
|
Type.File -> fileLoader(data)
|
||||||
|
null -> error("Invalid image")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun httpLoader(manga: Manga, options: Options): FetchResult {
|
||||||
|
// Only cache separately if it's a library item
|
||||||
|
val coverCacheFile = if (manga.favorite) {
|
||||||
|
coverCache.getCoverFile(manga) ?: error("No cover specified")
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (coverCacheFile?.exists() == true && options.diskCachePolicy.readEnabled) {
|
||||||
|
return fileLoader(coverCacheFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
val (response, body) = awaitGetCall(manga, options)
|
||||||
|
if (!response.isSuccessful) {
|
||||||
|
body.close()
|
||||||
|
throw HttpException(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (coverCacheFile != null && options.diskCachePolicy.writeEnabled) {
|
||||||
|
@Suppress("BlockingMethodInNonBlockingContext")
|
||||||
|
response.peekBody(Long.MAX_VALUE).source().use { input ->
|
||||||
|
coverCacheFile.parentFile?.mkdirs()
|
||||||
|
if (coverCacheFile.exists()) {
|
||||||
|
coverCacheFile.delete()
|
||||||
|
}
|
||||||
|
coverCacheFile.sink().buffer().use { output ->
|
||||||
|
output.writeAll(input)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return SourceResult(
|
||||||
|
source = body.source(),
|
||||||
|
mimeType = "image/*",
|
||||||
|
dataSource = if (response.cacheResponse != null) DataSource.DISK else DataSource.NETWORK
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun awaitGetCall(manga: Manga, options: Options): Pair<Response, ResponseBody> {
|
||||||
|
val call = getCall(manga, options)
|
||||||
|
val response = call.await()
|
||||||
|
return response to checkNotNull(response.body) { "Null response source" }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getCall(manga: Manga, options: Options): Call {
|
||||||
|
val source = sourceManager.get(manga.source) as? HttpSource
|
||||||
|
val request = Request.Builder().url(manga.thumbnail_url!!).also {
|
||||||
|
if (source != null) {
|
||||||
|
it.headers(source.headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
val networkRead = options.networkCachePolicy.readEnabled
|
||||||
|
val diskRead = options.diskCachePolicy.readEnabled
|
||||||
|
when {
|
||||||
|
!networkRead && diskRead -> {
|
||||||
|
it.cacheControl(CacheControl.FORCE_CACHE)
|
||||||
|
}
|
||||||
|
networkRead && !diskRead -> if (options.diskCachePolicy.writeEnabled) {
|
||||||
|
it.cacheControl(CacheControl.FORCE_NETWORK)
|
||||||
|
} else {
|
||||||
|
it.cacheControl(CACHE_CONTROL_FORCE_NETWORK_NO_CACHE)
|
||||||
|
}
|
||||||
|
!networkRead && !diskRead -> {
|
||||||
|
// This causes the request to fail with a 504 Unsatisfiable Request.
|
||||||
|
it.cacheControl(CACHE_CONTROL_NO_NETWORK_NO_CACHE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.build()
|
||||||
|
|
||||||
|
val client = source?.client?.newBuilder()?.cache(defaultClient.cache)?.build() ?: defaultClient
|
||||||
|
return client.newCall(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun fileLoader(manga: Manga): FetchResult {
|
||||||
|
return fileLoader(File(manga.thumbnail_url!!.substringAfter("file://")))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun fileLoader(file: File): FetchResult {
|
||||||
|
return SourceResult(
|
||||||
|
source = file.source().buffer(),
|
||||||
|
mimeType = "image/*",
|
||||||
|
dataSource = DataSource.DISK
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getResourceType(cover: String?): Type? {
|
||||||
|
return when {
|
||||||
|
cover.isNullOrEmpty() -> null
|
||||||
|
cover.startsWith("http", true) || cover.startsWith("Custom-", true) -> Type.URL
|
||||||
|
cover.startsWith("/") || cover.startsWith("file://") -> Type.File
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum class Type {
|
||||||
|
File, URL
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val USE_CUSTOM_COVER = "use_custom_cover"
|
||||||
|
|
||||||
|
private val CACHE_CONTROL_FORCE_NETWORK_NO_CACHE = CacheControl.Builder().noCache().noStore().build()
|
||||||
|
private val CACHE_CONTROL_NO_NETWORK_NO_CACHE = CacheControl.Builder().noCache().onlyIfCached().build()
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,53 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.coil
|
||||||
|
|
||||||
|
import android.content.res.Resources
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.core.graphics.drawable.toDrawable
|
||||||
|
import coil.bitmap.BitmapPool
|
||||||
|
import coil.decode.DecodeResult
|
||||||
|
import coil.decode.Decoder
|
||||||
|
import coil.decode.Options
|
||||||
|
import coil.size.Size
|
||||||
|
import eu.kanade.tachiyomi.util.system.ImageUtil
|
||||||
|
import okio.BufferedSource
|
||||||
|
import tachiyomi.decoder.ImageDecoder
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A [Decoder] that uses built-in [ImageDecoder] to decode images that is not supported by the system.
|
||||||
|
*/
|
||||||
|
class TachiyomiImageDecoder(private val resources: Resources) : Decoder {
|
||||||
|
|
||||||
|
override fun handles(source: BufferedSource, mimeType: String?): Boolean {
|
||||||
|
val type = source.peek().inputStream().use {
|
||||||
|
ImageUtil.findImageType(it)
|
||||||
|
}
|
||||||
|
return when (type) {
|
||||||
|
ImageUtil.ImageType.AVIF, ImageUtil.ImageType.JXL -> true
|
||||||
|
ImageUtil.ImageType.HEIF -> Build.VERSION.SDK_INT < Build.VERSION_CODES.O
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun decode(
|
||||||
|
pool: BitmapPool,
|
||||||
|
source: BufferedSource,
|
||||||
|
size: Size,
|
||||||
|
options: Options
|
||||||
|
): DecodeResult {
|
||||||
|
val decoder = source.use {
|
||||||
|
ImageDecoder.newInstance(it.inputStream())
|
||||||
|
}
|
||||||
|
|
||||||
|
check(decoder != null && decoder.width > 0 && decoder.height > 0) { "Failed to initialize decoder." }
|
||||||
|
|
||||||
|
val bitmap = decoder.decode(rgb565 = options.allowRgb565)
|
||||||
|
decoder.recycle()
|
||||||
|
|
||||||
|
check(bitmap != null) { "Failed to decode image." }
|
||||||
|
|
||||||
|
return DecodeResult(
|
||||||
|
drawable = bitmap.toDrawable(resources),
|
||||||
|
isSampled = false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -20,7 +20,7 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
|
|||||||
/**
|
/**
|
||||||
* Version of the database.
|
* Version of the database.
|
||||||
*/
|
*/
|
||||||
const val DATABASE_VERSION = 11
|
const val DATABASE_VERSION = 13
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate(db: SupportSQLiteDatabase) = with(db) {
|
override fun onCreate(db: SupportSQLiteDatabase) = with(db) {
|
||||||
@ -82,6 +82,15 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
|
|||||||
db.execSQL(MangaTable.addDateAdded)
|
db.execSQL(MangaTable.addDateAdded)
|
||||||
db.execSQL(MangaTable.backfillDateAdded)
|
db.execSQL(MangaTable.backfillDateAdded)
|
||||||
}
|
}
|
||||||
|
if (oldVersion < 12) {
|
||||||
|
db.execSQL(MangaTable.addNextUpdateCol)
|
||||||
|
}
|
||||||
|
if (oldVersion < 13) {
|
||||||
|
db.execSQL(TrackTable.renameTableToTemp)
|
||||||
|
db.execSQL(TrackTable.createTableQuery)
|
||||||
|
db.execSQL(TrackTable.insertFromTempTable)
|
||||||
|
db.execSQL(TrackTable.dropTempTable)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onConfigure(db: SupportSQLiteDatabase) {
|
override fun onConfigure(db: SupportSQLiteDatabase) {
|
||||||
|
@ -22,6 +22,7 @@ import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_GENRE
|
|||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_ID
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_ID
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_INITIALIZED
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_INITIALIZED
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_LAST_UPDATE
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_LAST_UPDATE
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_NEXT_UPDATE
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_SOURCE
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_SOURCE
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_STATUS
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_STATUS
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_THUMBNAIL_URL
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_THUMBNAIL_URL
|
||||||
@ -62,8 +63,9 @@ class MangaPutResolver : DefaultPutResolver<Manga>() {
|
|||||||
COL_THUMBNAIL_URL to obj.thumbnail_url,
|
COL_THUMBNAIL_URL to obj.thumbnail_url,
|
||||||
COL_FAVORITE to obj.favorite,
|
COL_FAVORITE to obj.favorite,
|
||||||
COL_LAST_UPDATE to obj.last_update,
|
COL_LAST_UPDATE to obj.last_update,
|
||||||
|
COL_NEXT_UPDATE to obj.next_update,
|
||||||
COL_INITIALIZED to obj.initialized,
|
COL_INITIALIZED to obj.initialized,
|
||||||
COL_VIEWER to obj.viewer,
|
COL_VIEWER to obj.viewer_flags,
|
||||||
COL_CHAPTER_FLAGS to obj.chapter_flags,
|
COL_CHAPTER_FLAGS to obj.chapter_flags,
|
||||||
COL_COVER_LAST_MODIFIED to obj.cover_last_modified,
|
COL_COVER_LAST_MODIFIED to obj.cover_last_modified,
|
||||||
COL_DATE_ADDED to obj.date_added
|
COL_DATE_ADDED to obj.date_added
|
||||||
@ -84,8 +86,9 @@ interface BaseMangaGetResolver {
|
|||||||
thumbnail_url = cursor.getString(cursor.getColumnIndex(COL_THUMBNAIL_URL))
|
thumbnail_url = cursor.getString(cursor.getColumnIndex(COL_THUMBNAIL_URL))
|
||||||
favorite = cursor.getInt(cursor.getColumnIndex(COL_FAVORITE)) == 1
|
favorite = cursor.getInt(cursor.getColumnIndex(COL_FAVORITE)) == 1
|
||||||
last_update = cursor.getLong(cursor.getColumnIndex(COL_LAST_UPDATE))
|
last_update = cursor.getLong(cursor.getColumnIndex(COL_LAST_UPDATE))
|
||||||
|
next_update = cursor.getLong(cursor.getColumnIndex(COL_NEXT_UPDATE))
|
||||||
initialized = cursor.getInt(cursor.getColumnIndex(COL_INITIALIZED)) == 1
|
initialized = cursor.getInt(cursor.getColumnIndex(COL_INITIALIZED)) == 1
|
||||||
viewer = cursor.getInt(cursor.getColumnIndex(COL_VIEWER))
|
viewer_flags = cursor.getInt(cursor.getColumnIndex(COL_VIEWER))
|
||||||
chapter_flags = cursor.getInt(cursor.getColumnIndex(COL_CHAPTER_FLAGS))
|
chapter_flags = cursor.getInt(cursor.getColumnIndex(COL_CHAPTER_FLAGS))
|
||||||
cover_last_modified = cursor.getLong(cursor.getColumnIndex(COL_COVER_LAST_MODIFIED))
|
cover_last_modified = cursor.getLong(cursor.getColumnIndex(COL_COVER_LAST_MODIFIED))
|
||||||
date_added = cursor.getLong(cursor.getColumnIndex(COL_DATE_ADDED))
|
date_added = cursor.getLong(cursor.getColumnIndex(COL_DATE_ADDED))
|
||||||
|
@ -71,7 +71,7 @@ class TrackGetResolver : DefaultGetResolver<Track>() {
|
|||||||
media_id = cursor.getInt(cursor.getColumnIndex(COL_MEDIA_ID))
|
media_id = cursor.getInt(cursor.getColumnIndex(COL_MEDIA_ID))
|
||||||
library_id = cursor.getLong(cursor.getColumnIndex(COL_LIBRARY_ID))
|
library_id = cursor.getLong(cursor.getColumnIndex(COL_LIBRARY_ID))
|
||||||
title = cursor.getString(cursor.getColumnIndex(COL_TITLE))
|
title = cursor.getString(cursor.getColumnIndex(COL_TITLE))
|
||||||
last_chapter_read = cursor.getInt(cursor.getColumnIndex(COL_LAST_CHAPTER_READ))
|
last_chapter_read = cursor.getFloat(cursor.getColumnIndex(COL_LAST_CHAPTER_READ))
|
||||||
total_chapters = cursor.getInt(cursor.getColumnIndex(COL_TOTAL_CHAPTERS))
|
total_chapters = cursor.getInt(cursor.getColumnIndex(COL_TOTAL_CHAPTERS))
|
||||||
status = cursor.getInt(cursor.getColumnIndex(COL_STATUS))
|
status = cursor.getInt(cursor.getColumnIndex(COL_STATUS))
|
||||||
score = cursor.getFloat(cursor.getColumnIndex(COL_SCORE))
|
score = cursor.getFloat(cursor.getColumnIndex(COL_SCORE))
|
||||||
|
@ -1,5 +1,10 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.models
|
package eu.kanade.tachiyomi.data.database.models
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.ui.library.setting.DisplayModeSetting
|
||||||
|
import eu.kanade.tachiyomi.ui.library.setting.SortDirectionSetting
|
||||||
|
import eu.kanade.tachiyomi.ui.library.setting.SortModeSetting
|
||||||
import java.io.Serializable
|
import java.io.Serializable
|
||||||
|
|
||||||
interface Category : Serializable {
|
interface Category : Serializable {
|
||||||
@ -12,12 +17,28 @@ interface Category : Serializable {
|
|||||||
|
|
||||||
var flags: Int
|
var flags: Int
|
||||||
|
|
||||||
|
private fun setFlags(flag: Int, mask: Int) {
|
||||||
|
flags = flags and mask.inv() or (flag and mask)
|
||||||
|
}
|
||||||
|
|
||||||
|
var displayMode: Int
|
||||||
|
get() = flags and DisplayModeSetting.MASK
|
||||||
|
set(mode) = setFlags(mode, DisplayModeSetting.MASK)
|
||||||
|
|
||||||
|
var sortMode: Int
|
||||||
|
get() = flags and SortModeSetting.MASK
|
||||||
|
set(mode) = setFlags(mode, SortModeSetting.MASK)
|
||||||
|
|
||||||
|
var sortDirection: Int
|
||||||
|
get() = flags and SortDirectionSetting.MASK
|
||||||
|
set(mode) = setFlags(mode, SortDirectionSetting.MASK)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
fun create(name: String): Category = CategoryImpl().apply {
|
fun create(name: String): Category = CategoryImpl().apply {
|
||||||
this.name = name
|
this.name = name
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createDefault(): Category = create("Default").apply { id = 0 }
|
fun createDefault(context: Context): Category = create(context.getString(R.string.label_default)).apply { id = 0 }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.models
|
package eu.kanade.tachiyomi.data.database.models
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
|
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
|
||||||
|
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
|
||||||
import tachiyomi.source.model.MangaInfo
|
import tachiyomi.source.model.MangaInfo
|
||||||
|
|
||||||
interface Manga : SManga {
|
interface Manga : SManga {
|
||||||
@ -11,82 +13,99 @@ interface Manga : SManga {
|
|||||||
|
|
||||||
var favorite: Boolean
|
var favorite: Boolean
|
||||||
|
|
||||||
|
// last time the chapter list changed in any way
|
||||||
var last_update: Long
|
var last_update: Long
|
||||||
|
|
||||||
|
// predicted next update time based on latest (by date) 4 chapters' deltas
|
||||||
|
var next_update: Long
|
||||||
|
|
||||||
var date_added: Long
|
var date_added: Long
|
||||||
|
|
||||||
var viewer: Int
|
var viewer_flags: Int
|
||||||
|
|
||||||
var chapter_flags: Int
|
var chapter_flags: Int
|
||||||
|
|
||||||
var cover_last_modified: Long
|
var cover_last_modified: Long
|
||||||
|
|
||||||
fun setChapterOrder(order: Int) {
|
fun setChapterOrder(order: Int) {
|
||||||
setFlags(order, SORT_MASK)
|
setChapterFlags(order, CHAPTER_SORT_MASK)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sortDescending(): Boolean {
|
fun sortDescending(): Boolean {
|
||||||
return chapter_flags and SORT_MASK == SORT_DESC
|
return chapter_flags and CHAPTER_SORT_MASK == CHAPTER_SORT_DESC
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getGenres(): List<String>? {
|
fun getGenres(): List<String>? {
|
||||||
return genre?.split(", ")?.map { it.trim() }
|
if (genre.isNullOrBlank()) return null
|
||||||
|
return genre?.split(", ")?.map { it.trim() }?.filterNot { it.isBlank() }?.distinct()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setFlags(flag: Int, mask: Int) {
|
private fun setChapterFlags(flag: Int, mask: Int) {
|
||||||
chapter_flags = chapter_flags and mask.inv() or (flag and mask)
|
chapter_flags = chapter_flags and mask.inv() or (flag and mask)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun setViewerFlags(flag: Int, mask: Int) {
|
||||||
|
viewer_flags = viewer_flags and mask.inv() or (flag and mask)
|
||||||
|
}
|
||||||
|
|
||||||
// Used to display the chapter's title one way or another
|
// Used to display the chapter's title one way or another
|
||||||
var displayMode: Int
|
var displayMode: Int
|
||||||
get() = chapter_flags and DISPLAY_MASK
|
get() = chapter_flags and CHAPTER_DISPLAY_MASK
|
||||||
set(mode) = setFlags(mode, DISPLAY_MASK)
|
set(mode) = setChapterFlags(mode, CHAPTER_DISPLAY_MASK)
|
||||||
|
|
||||||
var readFilter: Int
|
var readFilter: Int
|
||||||
get() = chapter_flags and READ_MASK
|
get() = chapter_flags and CHAPTER_READ_MASK
|
||||||
set(filter) = setFlags(filter, READ_MASK)
|
set(filter) = setChapterFlags(filter, CHAPTER_READ_MASK)
|
||||||
|
|
||||||
var downloadedFilter: Int
|
var downloadedFilter: Int
|
||||||
get() = chapter_flags and DOWNLOADED_MASK
|
get() = chapter_flags and CHAPTER_DOWNLOADED_MASK
|
||||||
set(filter) = setFlags(filter, DOWNLOADED_MASK)
|
set(filter) = setChapterFlags(filter, CHAPTER_DOWNLOADED_MASK)
|
||||||
|
|
||||||
var bookmarkedFilter: Int
|
var bookmarkedFilter: Int
|
||||||
get() = chapter_flags and BOOKMARKED_MASK
|
get() = chapter_flags and CHAPTER_BOOKMARKED_MASK
|
||||||
set(filter) = setFlags(filter, BOOKMARKED_MASK)
|
set(filter) = setChapterFlags(filter, CHAPTER_BOOKMARKED_MASK)
|
||||||
|
|
||||||
var sorting: Int
|
var sorting: Int
|
||||||
get() = chapter_flags and SORTING_MASK
|
get() = chapter_flags and CHAPTER_SORTING_MASK
|
||||||
set(sort) = setFlags(sort, SORTING_MASK)
|
set(sort) = setChapterFlags(sort, CHAPTER_SORTING_MASK)
|
||||||
|
|
||||||
|
var readingModeType: Int
|
||||||
|
get() = viewer_flags and ReadingModeType.MASK
|
||||||
|
set(readingMode) = setViewerFlags(readingMode, ReadingModeType.MASK)
|
||||||
|
|
||||||
|
var orientationType: Int
|
||||||
|
get() = viewer_flags and OrientationType.MASK
|
||||||
|
set(rotationType) = setViewerFlags(rotationType, OrientationType.MASK)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
const val SORT_DESC = 0x00000000
|
|
||||||
const val SORT_ASC = 0x00000001
|
|
||||||
const val SORT_MASK = 0x00000001
|
|
||||||
|
|
||||||
// Generic filter that does not filter anything
|
// Generic filter that does not filter anything
|
||||||
const val SHOW_ALL = 0x00000000
|
const val SHOW_ALL = 0x00000000
|
||||||
|
|
||||||
const val SHOW_UNREAD = 0x00000002
|
const val CHAPTER_SORT_DESC = 0x00000000
|
||||||
const val SHOW_READ = 0x00000004
|
const val CHAPTER_SORT_ASC = 0x00000001
|
||||||
const val READ_MASK = 0x00000006
|
const val CHAPTER_SORT_MASK = 0x00000001
|
||||||
|
|
||||||
const val SHOW_DOWNLOADED = 0x00000008
|
const val CHAPTER_SHOW_UNREAD = 0x00000002
|
||||||
const val SHOW_NOT_DOWNLOADED = 0x00000010
|
const val CHAPTER_SHOW_READ = 0x00000004
|
||||||
const val DOWNLOADED_MASK = 0x00000018
|
const val CHAPTER_READ_MASK = 0x00000006
|
||||||
|
|
||||||
const val SHOW_BOOKMARKED = 0x00000020
|
const val CHAPTER_SHOW_DOWNLOADED = 0x00000008
|
||||||
const val SHOW_NOT_BOOKMARKED = 0x00000040
|
const val CHAPTER_SHOW_NOT_DOWNLOADED = 0x00000010
|
||||||
const val BOOKMARKED_MASK = 0x00000060
|
const val CHAPTER_DOWNLOADED_MASK = 0x00000018
|
||||||
|
|
||||||
const val SORTING_SOURCE = 0x00000000
|
const val CHAPTER_SHOW_BOOKMARKED = 0x00000020
|
||||||
const val SORTING_NUMBER = 0x00000100
|
const val CHAPTER_SHOW_NOT_BOOKMARKED = 0x00000040
|
||||||
const val SORTING_UPLOAD_DATE = 0x00000200
|
const val CHAPTER_BOOKMARKED_MASK = 0x00000060
|
||||||
const val SORTING_MASK = 0x00000300
|
|
||||||
|
|
||||||
const val DISPLAY_NAME = 0x00000000
|
const val CHAPTER_SORTING_SOURCE = 0x00000000
|
||||||
const val DISPLAY_NUMBER = 0x00100000
|
const val CHAPTER_SORTING_NUMBER = 0x00000100
|
||||||
const val DISPLAY_MASK = 0x00100000
|
const val CHAPTER_SORTING_UPLOAD_DATE = 0x00000200
|
||||||
|
const val CHAPTER_SORTING_MASK = 0x00000300
|
||||||
|
|
||||||
|
const val CHAPTER_DISPLAY_NAME = 0x00000000
|
||||||
|
const val CHAPTER_DISPLAY_NUMBER = 0x00100000
|
||||||
|
const val CHAPTER_DISPLAY_MASK = 0x00100000
|
||||||
|
|
||||||
fun create(source: Long): Manga = MangaImpl().apply {
|
fun create(source: Long): Manga = MangaImpl().apply {
|
||||||
this.source = source
|
this.source = source
|
||||||
|
@ -26,11 +26,13 @@ open class MangaImpl : Manga {
|
|||||||
|
|
||||||
override var last_update: Long = 0
|
override var last_update: Long = 0
|
||||||
|
|
||||||
|
override var next_update: Long = 0
|
||||||
|
|
||||||
override var date_added: Long = 0
|
override var date_added: Long = 0
|
||||||
|
|
||||||
override var initialized: Boolean = false
|
override var initialized: Boolean = false
|
||||||
|
|
||||||
override var viewer: Int = 0
|
override var viewer_flags: Int = 0
|
||||||
|
|
||||||
override var chapter_flags: Int = 0
|
override var chapter_flags: Int = 0
|
||||||
|
|
||||||
|
@ -16,7 +16,7 @@ interface Track : Serializable {
|
|||||||
|
|
||||||
var title: String
|
var title: String
|
||||||
|
|
||||||
var last_chapter_read: Int
|
var last_chapter_read: Float
|
||||||
|
|
||||||
var total_chapters: Int
|
var total_chapters: Int
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@ class TrackImpl : Track {
|
|||||||
|
|
||||||
override lateinit var title: String
|
override lateinit var title: String
|
||||||
|
|
||||||
override var last_chapter_read: Int = 0
|
override var last_chapter_read: Float = 0F
|
||||||
|
|
||||||
override var total_chapters: Int = 0
|
override var total_chapters: Int = 0
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.queries
|
package eu.kanade.tachiyomi.data.database.queries
|
||||||
|
|
||||||
|
import com.pushtorefresh.storio.sqlite.operations.get.PreparedGetListOfObjects
|
||||||
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
|
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
|
||||||
import com.pushtorefresh.storio.sqlite.queries.Query
|
import com.pushtorefresh.storio.sqlite.queries.Query
|
||||||
import com.pushtorefresh.storio.sqlite.queries.RawQuery
|
import com.pushtorefresh.storio.sqlite.queries.RawQuery
|
||||||
@ -11,8 +12,8 @@ import eu.kanade.tachiyomi.data.database.resolvers.MangaCoverLastModifiedPutReso
|
|||||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaFavoritePutResolver
|
import eu.kanade.tachiyomi.data.database.resolvers.MangaFavoritePutResolver
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaFlagsPutResolver
|
import eu.kanade.tachiyomi.data.database.resolvers.MangaFlagsPutResolver
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaLastUpdatedPutResolver
|
import eu.kanade.tachiyomi.data.database.resolvers.MangaLastUpdatedPutResolver
|
||||||
|
import eu.kanade.tachiyomi.data.database.resolvers.MangaNextUpdatedPutResolver
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaTitlePutResolver
|
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.CategoryTable
|
||||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable
|
import eu.kanade.tachiyomi.data.database.tables.ChapterTable
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable
|
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable
|
||||||
@ -20,15 +21,6 @@ import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
|||||||
|
|
||||||
interface MangaQueries : DbProvider {
|
interface MangaQueries : DbProvider {
|
||||||
|
|
||||||
fun getMangas() = db.get()
|
|
||||||
.listOfObjects(Manga::class.java)
|
|
||||||
.withQuery(
|
|
||||||
Query.builder()
|
|
||||||
.table(MangaTable.TABLE)
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
.prepare()
|
|
||||||
|
|
||||||
fun getLibraryMangas() = db.get()
|
fun getLibraryMangas() = db.get()
|
||||||
.listOfObjects(LibraryManga::class.java)
|
.listOfObjects(LibraryManga::class.java)
|
||||||
.withQuery(
|
.withQuery(
|
||||||
@ -40,17 +32,21 @@ interface MangaQueries : DbProvider {
|
|||||||
.withGetResolver(LibraryMangaGetResolver.INSTANCE)
|
.withGetResolver(LibraryMangaGetResolver.INSTANCE)
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
fun getFavoriteMangas() = db.get()
|
fun getFavoriteMangas(sortByTitle: Boolean = true): PreparedGetListOfObjects<Manga> {
|
||||||
.listOfObjects(Manga::class.java)
|
var queryBuilder = Query.builder()
|
||||||
.withQuery(
|
.table(MangaTable.TABLE)
|
||||||
Query.builder()
|
.where("${MangaTable.COL_FAVORITE} = ?")
|
||||||
.table(MangaTable.TABLE)
|
.whereArgs(1)
|
||||||
.where("${MangaTable.COL_FAVORITE} = ?")
|
|
||||||
.whereArgs(1)
|
if (sortByTitle) {
|
||||||
.orderBy(MangaTable.COL_TITLE)
|
queryBuilder = queryBuilder.orderBy(MangaTable.COL_TITLE)
|
||||||
.build()
|
}
|
||||||
)
|
|
||||||
.prepare()
|
return db.get()
|
||||||
|
.listOfObjects(Manga::class.java)
|
||||||
|
.withQuery(queryBuilder.build())
|
||||||
|
.prepare()
|
||||||
|
}
|
||||||
|
|
||||||
fun getManga(url: String, sourceId: Long) = db.get()
|
fun getManga(url: String, sourceId: Long) = db.get()
|
||||||
.`object`(Manga::class.java)
|
.`object`(Manga::class.java)
|
||||||
@ -78,14 +74,29 @@ interface MangaQueries : DbProvider {
|
|||||||
|
|
||||||
fun insertMangas(mangas: List<Manga>) = db.put().objects(mangas).prepare()
|
fun insertMangas(mangas: List<Manga>) = db.put().objects(mangas).prepare()
|
||||||
|
|
||||||
fun updateFlags(manga: Manga) = db.put()
|
fun updateChapterFlags(manga: Manga) = db.put()
|
||||||
.`object`(manga)
|
.`object`(manga)
|
||||||
.withPutResolver(MangaFlagsPutResolver())
|
.withPutResolver(MangaFlagsPutResolver(MangaTable.COL_CHAPTER_FLAGS, Manga::chapter_flags))
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
fun updateFlags(mangas: List<Manga>) = db.put()
|
fun updateChapterFlags(manga: List<Manga>) = db.put()
|
||||||
.objects(mangas)
|
.objects(manga)
|
||||||
.withPutResolver(MangaFlagsPutResolver(true))
|
.withPutResolver(MangaFlagsPutResolver(MangaTable.COL_CHAPTER_FLAGS, Manga::chapter_flags))
|
||||||
|
.prepare()
|
||||||
|
|
||||||
|
fun updateViewerFlags(manga: Manga) = db.put()
|
||||||
|
.`object`(manga)
|
||||||
|
.withPutResolver(MangaFlagsPutResolver(MangaTable.COL_VIEWER, Manga::viewer_flags))
|
||||||
|
.prepare()
|
||||||
|
|
||||||
|
fun updateViewerFlags(manga: List<Manga>) = db.put()
|
||||||
|
.objects(manga)
|
||||||
|
.withPutResolver(MangaFlagsPutResolver(MangaTable.COL_VIEWER, Manga::viewer_flags))
|
||||||
|
.prepare()
|
||||||
|
|
||||||
|
fun updateNextUpdated(manga: Manga) = db.put()
|
||||||
|
.`object`(manga)
|
||||||
|
.withPutResolver(MangaNextUpdatedPutResolver())
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
fun updateLastUpdated(manga: Manga) = db.put()
|
fun updateLastUpdated(manga: Manga) = db.put()
|
||||||
@ -98,11 +109,6 @@ interface MangaQueries : DbProvider {
|
|||||||
.withPutResolver(MangaFavoritePutResolver())
|
.withPutResolver(MangaFavoritePutResolver())
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
fun updateMangaViewer(manga: Manga) = db.put()
|
|
||||||
.`object`(manga)
|
|
||||||
.withPutResolver(MangaViewerPutResolver())
|
|
||||||
.prepare()
|
|
||||||
|
|
||||||
fun updateMangaTitle(manga: Manga) = db.put()
|
fun updateMangaTitle(manga: Manga) = db.put()
|
||||||
.`object`(manga)
|
.`object`(manga)
|
||||||
.withPutResolver(MangaTitlePutResolver())
|
.withPutResolver(MangaTitlePutResolver())
|
||||||
@ -164,4 +170,14 @@ interface MangaQueries : DbProvider {
|
|||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
|
fun getChapterFetchDateManga() = db.get()
|
||||||
|
.listOfObjects(Manga::class.java)
|
||||||
|
.withQuery(
|
||||||
|
RawQuery.builder()
|
||||||
|
.query(getChapterFetchDateMangaQuery())
|
||||||
|
.observesTables(MangaTable.TABLE)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
.prepare()
|
||||||
}
|
}
|
||||||
|
@ -122,6 +122,16 @@ fun getLatestChapterMangaQuery() =
|
|||||||
ORDER by max DESC
|
ORDER by max DESC
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
fun getChapterFetchDateMangaQuery() =
|
||||||
|
"""
|
||||||
|
SELECT ${Manga.TABLE}.*, MAX(${Chapter.TABLE}.${Chapter.COL_DATE_FETCH}) 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.
|
* Query to get the categories for a manga.
|
||||||
*/
|
*/
|
||||||
|
@ -27,9 +27,7 @@ class HistoryLastReadPutResolver : HistoryPutResolver() {
|
|||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
|
|
||||||
val putResult: PutResult
|
cursor.use { putCursor ->
|
||||||
|
|
||||||
putResult = cursor.use { putCursor ->
|
|
||||||
if (putCursor.count == 0) {
|
if (putCursor.count == 0) {
|
||||||
val insertQuery = mapToInsertQuery(history)
|
val insertQuery = mapToInsertQuery(history)
|
||||||
val insertedId = db.lowLevel().insert(insertQuery, mapToContentValues(history))
|
val insertedId = db.lowLevel().insert(insertQuery, mapToContentValues(history))
|
||||||
@ -39,25 +37,15 @@ class HistoryLastReadPutResolver : HistoryPutResolver() {
|
|||||||
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
|
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
putResult
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates update query
|
|
||||||
* @param obj history object
|
|
||||||
*/
|
|
||||||
override fun mapToUpdateQuery(obj: History) = UpdateQuery.builder()
|
override fun mapToUpdateQuery(obj: History) = UpdateQuery.builder()
|
||||||
.table(HistoryTable.TABLE)
|
.table(HistoryTable.TABLE)
|
||||||
.where("${HistoryTable.COL_CHAPTER_ID} = ?")
|
.where("${HistoryTable.COL_CHAPTER_ID} = ?")
|
||||||
.whereArgs(obj.chapter_id)
|
.whereArgs(obj.chapter_id)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
/**
|
private fun mapToUpdateContentValues(history: History) =
|
||||||
* Create content query
|
|
||||||
* @param history object
|
|
||||||
*/
|
|
||||||
fun mapToUpdateContentValues(history: History) =
|
|
||||||
contentValuesOf(
|
contentValuesOf(
|
||||||
HistoryTable.COL_LAST_READ to history.last_read
|
HistoryTable.COL_LAST_READ to history.last_read
|
||||||
)
|
)
|
||||||
|
@ -8,8 +8,9 @@ import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
|
|||||||
import eu.kanade.tachiyomi.data.database.inTransactionReturn
|
import eu.kanade.tachiyomi.data.database.inTransactionReturn
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
||||||
|
import kotlin.reflect.KProperty1
|
||||||
|
|
||||||
class MangaFlagsPutResolver(private val updateAll: Boolean = false) : PutResolver<Manga>() {
|
class MangaFlagsPutResolver(private val colName: String, private val fieldGetter: KProperty1<Manga, Int>) : PutResolver<Manga>() {
|
||||||
|
|
||||||
override fun performPut(db: StorIOSQLite, manga: Manga) = db.inTransactionReturn {
|
override fun performPut(db: StorIOSQLite, manga: Manga) = db.inTransactionReturn {
|
||||||
val updateQuery = mapToUpdateQuery(manga)
|
val updateQuery = mapToUpdateQuery(manga)
|
||||||
@ -19,24 +20,14 @@ class MangaFlagsPutResolver(private val updateAll: Boolean = false) : PutResolve
|
|||||||
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
|
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun mapToUpdateQuery(manga: Manga): UpdateQuery {
|
fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder()
|
||||||
val builder = UpdateQuery.builder()
|
.table(MangaTable.TABLE)
|
||||||
|
.where("${MangaTable.COL_ID} = ?")
|
||||||
return if (updateAll) {
|
.whereArgs(manga.id)
|
||||||
builder
|
.build()
|
||||||
.table(MangaTable.TABLE)
|
|
||||||
.build()
|
|
||||||
} else {
|
|
||||||
builder
|
|
||||||
.table(MangaTable.TABLE)
|
|
||||||
.where("${MangaTable.COL_ID} = ?")
|
|
||||||
.whereArgs(manga.id)
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun mapToContentValues(manga: Manga) =
|
fun mapToContentValues(manga: Manga) =
|
||||||
contentValuesOf(
|
contentValuesOf(
|
||||||
MangaTable.COL_CHAPTER_FLAGS to manga.chapter_flags
|
colName to fieldGetter.get(manga)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,7 @@ import eu.kanade.tachiyomi.data.database.inTransactionReturn
|
|||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
||||||
|
|
||||||
class MangaViewerPutResolver : PutResolver<Manga>() {
|
class MangaNextUpdatedPutResolver : PutResolver<Manga>() {
|
||||||
|
|
||||||
override fun performPut(db: StorIOSQLite, manga: Manga) = db.inTransactionReturn {
|
override fun performPut(db: StorIOSQLite, manga: Manga) = db.inTransactionReturn {
|
||||||
val updateQuery = mapToUpdateQuery(manga)
|
val updateQuery = mapToUpdateQuery(manga)
|
||||||
@ -25,8 +25,7 @@ class MangaViewerPutResolver : PutResolver<Manga>() {
|
|||||||
.whereArgs(manga.id)
|
.whereArgs(manga.id)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
fun mapToContentValues(manga: Manga) =
|
fun mapToContentValues(manga: Manga) = contentValuesOf(
|
||||||
contentValuesOf(
|
MangaTable.COL_NEXT_UPDATE to manga.next_update
|
||||||
MangaTable.COL_VIEWER to manga.viewer
|
)
|
||||||
)
|
|
||||||
}
|
}
|
@ -28,6 +28,8 @@ object MangaTable {
|
|||||||
|
|
||||||
const val COL_LAST_UPDATE = "last_update"
|
const val COL_LAST_UPDATE = "last_update"
|
||||||
|
|
||||||
|
const val COL_NEXT_UPDATE = "next_update"
|
||||||
|
|
||||||
const val COL_DATE_ADDED = "date_added"
|
const val COL_DATE_ADDED = "date_added"
|
||||||
|
|
||||||
const val COL_INITIALIZED = "initialized"
|
const val COL_INITIALIZED = "initialized"
|
||||||
@ -57,6 +59,7 @@ object MangaTable {
|
|||||||
$COL_THUMBNAIL_URL TEXT,
|
$COL_THUMBNAIL_URL TEXT,
|
||||||
$COL_FAVORITE INTEGER NOT NULL,
|
$COL_FAVORITE INTEGER NOT NULL,
|
||||||
$COL_LAST_UPDATE LONG,
|
$COL_LAST_UPDATE LONG,
|
||||||
|
$COL_NEXT_UPDATE LONG,
|
||||||
$COL_INITIALIZED BOOLEAN NOT NULL,
|
$COL_INITIALIZED BOOLEAN NOT NULL,
|
||||||
$COL_VIEWER INTEGER NOT NULL,
|
$COL_VIEWER INTEGER NOT NULL,
|
||||||
$COL_CHAPTER_FLAGS INTEGER NOT NULL,
|
$COL_CHAPTER_FLAGS INTEGER NOT NULL,
|
||||||
@ -86,4 +89,7 @@ object MangaTable {
|
|||||||
"FROM $TABLE INNER JOIN ${ChapterTable.TABLE} " +
|
"FROM $TABLE INNER JOIN ${ChapterTable.TABLE} " +
|
||||||
"ON $TABLE.$COL_ID = ${ChapterTable.TABLE}.${ChapterTable.COL_MANGA_ID} " +
|
"ON $TABLE.$COL_ID = ${ChapterTable.TABLE}.${ChapterTable.COL_MANGA_ID} " +
|
||||||
"GROUP BY $TABLE.$COL_ID)"
|
"GROUP BY $TABLE.$COL_ID)"
|
||||||
|
|
||||||
|
val addNextUpdateCol: String
|
||||||
|
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_NEXT_UPDATE LONG DEFAULT 0"
|
||||||
}
|
}
|
||||||
|
@ -39,7 +39,7 @@ object TrackTable {
|
|||||||
$COL_MEDIA_ID INTEGER NOT NULL,
|
$COL_MEDIA_ID INTEGER NOT NULL,
|
||||||
$COL_LIBRARY_ID INTEGER,
|
$COL_LIBRARY_ID INTEGER,
|
||||||
$COL_TITLE TEXT NOT NULL,
|
$COL_TITLE TEXT NOT NULL,
|
||||||
$COL_LAST_CHAPTER_READ INTEGER NOT NULL,
|
$COL_LAST_CHAPTER_READ REAL NOT NULL,
|
||||||
$COL_TOTAL_CHAPTERS INTEGER NOT NULL,
|
$COL_TOTAL_CHAPTERS INTEGER NOT NULL,
|
||||||
$COL_STATUS INTEGER NOT NULL,
|
$COL_STATUS INTEGER NOT NULL,
|
||||||
$COL_SCORE FLOAT NOT NULL,
|
$COL_SCORE FLOAT NOT NULL,
|
||||||
@ -62,4 +62,19 @@ object TrackTable {
|
|||||||
|
|
||||||
val addFinishDate: String
|
val addFinishDate: String
|
||||||
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_FINISH_DATE LONG NOT NULL DEFAULT 0"
|
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_FINISH_DATE LONG NOT NULL DEFAULT 0"
|
||||||
|
|
||||||
|
val renameTableToTemp: String
|
||||||
|
get() =
|
||||||
|
"ALTER TABLE $TABLE RENAME TO ${TABLE}_tmp"
|
||||||
|
|
||||||
|
val insertFromTempTable: String
|
||||||
|
get() =
|
||||||
|
"""
|
||||||
|
|INSERT INTO $TABLE($COL_ID,$COL_MANGA_ID,$COL_SYNC_ID,$COL_MEDIA_ID,$COL_LIBRARY_ID,$COL_TITLE,$COL_LAST_CHAPTER_READ,$COL_TOTAL_CHAPTERS,$COL_STATUS,$COL_SCORE,$COL_TRACKING_URL,$COL_START_DATE,$COL_FINISH_DATE)
|
||||||
|
|SELECT $COL_ID,$COL_MANGA_ID,$COL_SYNC_ID,$COL_MEDIA_ID,$COL_LIBRARY_ID,$COL_TITLE,$COL_LAST_CHAPTER_READ,$COL_TOTAL_CHAPTERS,$COL_STATUS,$COL_SCORE,$COL_TRACKING_URL,$COL_START_DATE,$COL_FINISH_DATE
|
||||||
|
|FROM ${TABLE}_tmp
|
||||||
|
""".trimMargin()
|
||||||
|
|
||||||
|
val dropTempTable: String
|
||||||
|
get() = "DROP TABLE ${TABLE}_tmp"
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ import android.content.Context
|
|||||||
import com.hippo.unifile.UniFile
|
import com.hippo.unifile.UniFile
|
||||||
import com.jakewharton.rxrelay.BehaviorRelay
|
import com.jakewharton.rxrelay.BehaviorRelay
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.download.model.Download
|
import eu.kanade.tachiyomi.data.download.model.Download
|
||||||
@ -12,8 +13,11 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
|||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
|
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -23,7 +27,10 @@ import uy.kohesive.injekt.injectLazy
|
|||||||
*
|
*
|
||||||
* @param context the application context.
|
* @param context the application context.
|
||||||
*/
|
*/
|
||||||
class DownloadManager(private val context: Context) {
|
class DownloadManager(
|
||||||
|
private val context: Context,
|
||||||
|
private val db: DatabaseHelper = Injekt.get()
|
||||||
|
) {
|
||||||
|
|
||||||
private val sourceManager: SourceManager by injectLazy()
|
private val sourceManager: SourceManager by injectLazy()
|
||||||
private val preferences: PreferencesHelper by injectLazy()
|
private val preferences: PreferencesHelper by injectLazy()
|
||||||
@ -94,6 +101,23 @@ class DownloadManager(private val context: Context) {
|
|||||||
downloader.clearQueue(isNotification)
|
downloader.clearQueue(isNotification)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun startDownloadNow(chapter: Chapter) {
|
||||||
|
val download = downloader.queue.find { it.chapter.id == chapter.id } ?: return
|
||||||
|
val queue = downloader.queue.toMutableList()
|
||||||
|
queue.remove(download)
|
||||||
|
queue.add(0, download)
|
||||||
|
reorderQueue(queue)
|
||||||
|
if (isPaused()) {
|
||||||
|
if (DownloadService.isRunning(context)) {
|
||||||
|
downloader.start()
|
||||||
|
} else {
|
||||||
|
DownloadService.start(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isPaused() = downloader.isPaused()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reorders the download queue.
|
* Reorders the download queue.
|
||||||
*
|
*
|
||||||
@ -199,7 +223,16 @@ class DownloadManager(private val context: Context) {
|
|||||||
* @param download the download to cancel.
|
* @param download the download to cancel.
|
||||||
*/
|
*/
|
||||||
fun deletePendingDownload(download: Download) {
|
fun deletePendingDownload(download: Download) {
|
||||||
deleteChapters(listOf(download.chapter), download.manga, download.source)
|
deleteChapters(listOf(download.chapter), download.manga, download.source, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deletePendingDownloads(vararg downloads: Download) {
|
||||||
|
val downloadsByManga = downloads.groupBy { it.manga.id }
|
||||||
|
downloadsByManga.map { entry ->
|
||||||
|
val manga = entry.value.first().manga
|
||||||
|
val source = entry.value.first().source
|
||||||
|
deleteChapters(entry.value.map { it.chapter }, manga, source, true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -208,19 +241,25 @@ class DownloadManager(private val context: Context) {
|
|||||||
* @param chapters the list of chapters to delete.
|
* @param chapters the list of chapters to delete.
|
||||||
* @param manga the manga of the chapters.
|
* @param manga the manga of the chapters.
|
||||||
* @param source the source of the chapters.
|
* @param source the source of the chapters.
|
||||||
|
* @param isCancelling true if it's simply cancelling a download
|
||||||
*/
|
*/
|
||||||
fun deleteChapters(chapters: List<Chapter>, manga: Manga, source: Source): List<Chapter> {
|
fun deleteChapters(chapters: List<Chapter>, manga: Manga, source: Source, isCancelling: Boolean = false): List<Chapter> {
|
||||||
val filteredChapters = getChaptersToDelete(chapters)
|
val filteredChapters = if (isCancelling) {
|
||||||
|
chapters
|
||||||
removeFromDownloadQueue(filteredChapters)
|
} else {
|
||||||
|
getChaptersToDelete(chapters, manga)
|
||||||
val chapterDirs = provider.findChapterDirs(filteredChapters, manga, source)
|
|
||||||
chapterDirs.forEach { it.delete() }
|
|
||||||
cache.removeChapters(filteredChapters, manga)
|
|
||||||
if (cache.getDownloadCount(manga) == 0) { // Delete manga directory if empty
|
|
||||||
chapterDirs.firstOrNull()?.parentFile?.delete()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
launchIO {
|
||||||
|
removeFromDownloadQueue(filteredChapters)
|
||||||
|
|
||||||
|
val chapterDirs = provider.findChapterDirs(filteredChapters, manga, source)
|
||||||
|
chapterDirs.forEach { it.delete() }
|
||||||
|
cache.removeChapters(filteredChapters, manga)
|
||||||
|
if (cache.getDownloadCount(manga) == 0) { // Delete manga directory if empty
|
||||||
|
chapterDirs.firstOrNull()?.parentFile?.delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
return filteredChapters
|
return filteredChapters
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -249,9 +288,11 @@ class DownloadManager(private val context: Context) {
|
|||||||
* @param source the source of the manga.
|
* @param source the source of the manga.
|
||||||
*/
|
*/
|
||||||
fun deleteManga(manga: Manga, source: Source) {
|
fun deleteManga(manga: Manga, source: Source) {
|
||||||
downloader.queue.remove(manga)
|
launchIO {
|
||||||
provider.findMangaDir(manga, source)?.delete()
|
downloader.queue.remove(manga)
|
||||||
cache.removeManga(manga)
|
provider.findMangaDir(manga, source)?.delete()
|
||||||
|
cache.removeManga(manga)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -261,7 +302,7 @@ class DownloadManager(private val context: Context) {
|
|||||||
* @param manga the manga of the chapters.
|
* @param manga the manga of the chapters.
|
||||||
*/
|
*/
|
||||||
fun enqueueDeleteChapters(chapters: List<Chapter>, manga: Manga) {
|
fun enqueueDeleteChapters(chapters: List<Chapter>, manga: Manga) {
|
||||||
pendingDeleter.addChapters(getChaptersToDelete(chapters), manga)
|
pendingDeleter.addChapters(getChaptersToDelete(chapters, manga), manga)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -301,8 +342,17 @@ class DownloadManager(private val context: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getChaptersToDelete(chapters: List<Chapter>): List<Chapter> {
|
private fun getChaptersToDelete(chapters: List<Chapter>, manga: Manga): List<Chapter> {
|
||||||
return if (!preferences.removeBookmarkedChapters()) {
|
// Retrieve the categories that are set to exclude from being deleted on read
|
||||||
|
val categoriesToExclude = preferences.removeExcludeCategories().get().map(String::toInt)
|
||||||
|
val categoriesForManga = db.getCategoriesForManga(manga).executeAsBlocking()
|
||||||
|
.mapNotNull { it.id }
|
||||||
|
.takeUnless { it.isEmpty() }
|
||||||
|
?: listOf(0)
|
||||||
|
|
||||||
|
return if (categoriesForManga.intersect(categoriesToExclude).isNotEmpty()) {
|
||||||
|
chapters.filterNot { it.read }
|
||||||
|
} else if (!preferences.removeBookmarkedChapters()) {
|
||||||
chapters.filterNot { it.bookmark }
|
chapters.filterNot { it.bookmark }
|
||||||
} else {
|
} else {
|
||||||
chapters
|
chapters
|
||||||
|
@ -27,6 +27,8 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
private val progressNotificationBuilder by lazy {
|
private val progressNotificationBuilder by lazy {
|
||||||
context.notificationBuilder(Notifications.CHANNEL_DOWNLOADER_PROGRESS) {
|
context.notificationBuilder(Notifications.CHANNEL_DOWNLOADER_PROGRESS) {
|
||||||
setLargeIcon(BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher))
|
setLargeIcon(BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher))
|
||||||
|
setAutoCancel(false)
|
||||||
|
setOnlyAlertOnce(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,7 +52,7 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
/**
|
/**
|
||||||
* Updated when error is thrown
|
* Updated when error is thrown
|
||||||
*/
|
*/
|
||||||
var errorThrown = false
|
private var errorThrown = false
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updated when paused
|
* Updated when paused
|
||||||
@ -81,10 +83,8 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
*/
|
*/
|
||||||
fun onProgressChange(download: Download) {
|
fun onProgressChange(download: Download) {
|
||||||
with(progressNotificationBuilder) {
|
with(progressNotificationBuilder) {
|
||||||
// Check if first call.
|
|
||||||
if (!isDownloading) {
|
if (!isDownloading) {
|
||||||
setSmallIcon(android.R.drawable.stat_sys_download)
|
setSmallIcon(android.R.drawable.stat_sys_download)
|
||||||
setAutoCancel(false)
|
|
||||||
clearActions()
|
clearActions()
|
||||||
// Open download manager when clicked
|
// Open download manager when clicked
|
||||||
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
||||||
@ -114,6 +114,7 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setProgress(download.pages!!.size, download.downloadedImages, false)
|
setProgress(download.pages!!.size, download.downloadedImages, false)
|
||||||
|
setOngoing(true)
|
||||||
|
|
||||||
show(Notifications.ID_DOWNLOAD_CHAPTER_PROGRESS)
|
show(Notifications.ID_DOWNLOAD_CHAPTER_PROGRESS)
|
||||||
}
|
}
|
||||||
@ -127,8 +128,8 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
setContentTitle(context.getString(R.string.chapter_paused))
|
setContentTitle(context.getString(R.string.chapter_paused))
|
||||||
setContentText(context.getString(R.string.download_notifier_download_paused))
|
setContentText(context.getString(R.string.download_notifier_download_paused))
|
||||||
setSmallIcon(R.drawable.ic_pause_24dp)
|
setSmallIcon(R.drawable.ic_pause_24dp)
|
||||||
setAutoCancel(false)
|
|
||||||
setProgress(0, 0, false)
|
setProgress(0, 0, false)
|
||||||
|
setOngoing(false)
|
||||||
clearActions()
|
clearActions()
|
||||||
// Open download manager when clicked
|
// Open download manager when clicked
|
||||||
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
||||||
@ -217,7 +218,6 @@ internal class DownloadNotifier(private val context: Context) {
|
|||||||
setContentText(error ?: context.getString(R.string.download_notifier_unknown_error))
|
setContentText(error ?: context.getString(R.string.download_notifier_unknown_error))
|
||||||
setSmallIcon(android.R.drawable.stat_sys_warning)
|
setSmallIcon(android.R.drawable.stat_sys_warning)
|
||||||
clearActions()
|
clearActions()
|
||||||
setAutoCancel(false)
|
|
||||||
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
|
||||||
setProgress(0, 0, false)
|
setProgress(0, 0, false)
|
||||||
|
|
||||||
|
@ -53,8 +53,8 @@ class DownloadProvider(private val context: Context) {
|
|||||||
return downloadsDir
|
return downloadsDir
|
||||||
.createDirectory(getSourceDirName(source))
|
.createDirectory(getSourceDirName(source))
|
||||||
.createDirectory(getMangaDirName(manga))
|
.createDirectory(getMangaDirName(manga))
|
||||||
} catch (e: NullPointerException) {
|
} catch (e: Throwable) {
|
||||||
Timber.w(e)
|
Timber.e(e, "Invalid download directory")
|
||||||
throw Exception(context.getString(R.string.invalid_download_dir))
|
throw Exception(context.getString(R.string.invalid_download_dir))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -65,7 +65,7 @@ class DownloadProvider(private val context: Context) {
|
|||||||
* @param source the source to query.
|
* @param source the source to query.
|
||||||
*/
|
*/
|
||||||
fun findSourceDir(source: Source): UniFile? {
|
fun findSourceDir(source: Source): UniFile? {
|
||||||
return downloadsDir.findFile(getSourceDirName(source))
|
return downloadsDir.findFile(getSourceDirName(source), true)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -76,7 +76,7 @@ class DownloadProvider(private val context: Context) {
|
|||||||
*/
|
*/
|
||||||
fun findMangaDir(manga: Manga, source: Source): UniFile? {
|
fun findMangaDir(manga: Manga, source: Source): UniFile? {
|
||||||
val sourceDir = findSourceDir(source)
|
val sourceDir = findSourceDir(source)
|
||||||
return sourceDir?.findFile(getMangaDirName(manga))
|
return sourceDir?.findFile(getMangaDirName(manga), true)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -89,7 +89,7 @@ class DownloadProvider(private val context: Context) {
|
|||||||
fun findChapterDir(chapter: Chapter, manga: Manga, source: Source): UniFile? {
|
fun findChapterDir(chapter: Chapter, manga: Manga, source: Source): UniFile? {
|
||||||
val mangaDir = findMangaDir(manga, source)
|
val mangaDir = findMangaDir(manga, source)
|
||||||
return getValidChapterDirNames(chapter).asSequence()
|
return getValidChapterDirNames(chapter).asSequence()
|
||||||
.mapNotNull { mangaDir?.findFile(it) }
|
.mapNotNull { mangaDir?.findFile(it, true) }
|
||||||
.firstOrNull()
|
.firstOrNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -115,7 +115,7 @@ class DownloadProvider(private val context: Context) {
|
|||||||
* @param source the source to query.
|
* @param source the source to query.
|
||||||
*/
|
*/
|
||||||
fun getSourceDirName(source: Source): String {
|
fun getSourceDirName(source: Source): String {
|
||||||
return source.toString()
|
return DiskUtil.buildValidFilename(source.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -150,6 +150,7 @@ class DownloadProvider(private val context: Context) {
|
|||||||
return listOf(
|
return listOf(
|
||||||
getChapterDirName(chapter),
|
getChapterDirName(chapter),
|
||||||
|
|
||||||
|
// TODO: remove this
|
||||||
// Legacy chapter directory name used in v0.9.2 and before
|
// Legacy chapter directory name used in v0.9.2 and before
|
||||||
DiskUtil.buildValidFilename(chapter.name)
|
DiskUtil.buildValidFilename(chapter.name)
|
||||||
)
|
)
|
||||||
|
@ -4,26 +4,32 @@ import android.app.Notification
|
|||||||
import android.app.Service
|
import android.app.Service
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.ConnectivityManager
|
|
||||||
import android.net.NetworkInfo.State.CONNECTED
|
|
||||||
import android.net.NetworkInfo.State.DISCONNECTED
|
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.os.PowerManager
|
import android.os.PowerManager
|
||||||
|
import androidx.annotation.StringRes
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import com.github.pwittchen.reactivenetwork.library.Connectivity
|
|
||||||
import com.github.pwittchen.reactivenetwork.library.ReactiveNetwork
|
|
||||||
import com.jakewharton.rxrelay.BehaviorRelay
|
import com.jakewharton.rxrelay.BehaviorRelay
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.util.lang.plusAssign
|
import eu.kanade.tachiyomi.util.lang.plusAssign
|
||||||
|
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||||
import eu.kanade.tachiyomi.util.system.acquireWakeLock
|
import eu.kanade.tachiyomi.util.system.acquireWakeLock
|
||||||
import eu.kanade.tachiyomi.util.system.connectivityManager
|
import eu.kanade.tachiyomi.util.system.isConnectedToWifi
|
||||||
|
import eu.kanade.tachiyomi.util.system.isOnline
|
||||||
|
import eu.kanade.tachiyomi.util.system.isServiceRunning
|
||||||
import eu.kanade.tachiyomi.util.system.notification
|
import eu.kanade.tachiyomi.util.system.notification
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import rx.schedulers.Schedulers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.cancel
|
||||||
|
import kotlinx.coroutines.flow.catch
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import ru.beryukhov.reactivenetwork.ReactiveNetwork
|
||||||
import rx.subscriptions.CompositeSubscription
|
import rx.subscriptions.CompositeSubscription
|
||||||
|
import timber.log.Timber
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -58,6 +64,16 @@ class DownloadService : Service() {
|
|||||||
fun stop(context: Context) {
|
fun stop(context: Context) {
|
||||||
context.stopService(Intent(context, DownloadService::class.java))
|
context.stopService(Intent(context, DownloadService::class.java))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 {
|
||||||
|
return context.isServiceRunning(DownloadService::class.java)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val downloadManager: DownloadManager by injectLazy()
|
private val downloadManager: DownloadManager by injectLazy()
|
||||||
@ -69,16 +85,15 @@ class DownloadService : Service() {
|
|||||||
*/
|
*/
|
||||||
private lateinit var wakeLock: PowerManager.WakeLock
|
private lateinit var wakeLock: PowerManager.WakeLock
|
||||||
|
|
||||||
/**
|
|
||||||
* Subscriptions to store while the service is running.
|
|
||||||
*/
|
|
||||||
private lateinit var subscriptions: CompositeSubscription
|
private lateinit var subscriptions: CompositeSubscription
|
||||||
|
private lateinit var ioScope: CoroutineScope
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when the service is created.
|
* Called when the service is created.
|
||||||
*/
|
*/
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||||
startForeground(Notifications.ID_DOWNLOAD_CHAPTER_PROGRESS, getPlaceholderNotification())
|
startForeground(Notifications.ID_DOWNLOAD_CHAPTER_PROGRESS, getPlaceholderNotification())
|
||||||
wakeLock = acquireWakeLock(javaClass.name)
|
wakeLock = acquireWakeLock(javaClass.name)
|
||||||
runningRelay.call(true)
|
runningRelay.call(true)
|
||||||
@ -91,6 +106,7 @@ class DownloadService : Service() {
|
|||||||
* Called when the service is destroyed.
|
* Called when the service is destroyed.
|
||||||
*/
|
*/
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
|
ioScope?.cancel()
|
||||||
runningRelay.call(false)
|
runningRelay.call(false)
|
||||||
subscriptions.unsubscribe()
|
subscriptions.unsubscribe()
|
||||||
downloadManager.stopDownloads()
|
downloadManager.stopDownloads()
|
||||||
@ -118,44 +134,43 @@ class DownloadService : Service() {
|
|||||||
* @see onNetworkStateChanged
|
* @see onNetworkStateChanged
|
||||||
*/
|
*/
|
||||||
private fun listenNetworkChanges() {
|
private fun listenNetworkChanges() {
|
||||||
subscriptions += ReactiveNetwork.observeNetworkConnectivity(applicationContext)
|
ReactiveNetwork()
|
||||||
.subscribeOn(Schedulers.io())
|
.observeNetworkConnectivity(applicationContext)
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.onEach {
|
||||||
.subscribe(
|
withUIContext {
|
||||||
{ state ->
|
onNetworkStateChanged()
|
||||||
onNetworkStateChanged(state)
|
}
|
||||||
},
|
}
|
||||||
{
|
.catch { error ->
|
||||||
|
withUIContext {
|
||||||
|
Timber.e(error)
|
||||||
toast(R.string.download_queue_error)
|
toast(R.string.download_queue_error)
|
||||||
stopSelf()
|
stopSelf()
|
||||||
}
|
}
|
||||||
)
|
}
|
||||||
|
.launchIn(ioScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when the network state changes.
|
* Called when the network state changes.
|
||||||
*
|
|
||||||
* @param connectivity the new network state.
|
|
||||||
*/
|
*/
|
||||||
private fun onNetworkStateChanged(connectivity: Connectivity) {
|
private fun onNetworkStateChanged() {
|
||||||
when (connectivity.state) {
|
if (isOnline()) {
|
||||||
CONNECTED -> {
|
if (preferences.downloadOnlyOverWifi() && !isConnectedToWifi()) {
|
||||||
if (preferences.downloadOnlyOverWifi() && connectivityManager.activeNetworkInfo?.type != ConnectivityManager.TYPE_WIFI) {
|
stopDownloads(R.string.download_notifier_text_only_wifi)
|
||||||
downloadManager.stopDownloads(getString(R.string.download_notifier_text_only_wifi))
|
} else {
|
||||||
} else {
|
val started = downloadManager.startDownloads()
|
||||||
val started = downloadManager.startDownloads()
|
if (!started) stopSelf()
|
||||||
if (!started) stopSelf()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
DISCONNECTED -> {
|
|
||||||
downloadManager.stopDownloads(getString(R.string.download_notifier_no_network))
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
/* Do nothing */
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
stopDownloads(R.string.download_notifier_no_network)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun stopDownloads(@StringRes string: Int) {
|
||||||
|
downloadManager.stopDownloads(getString(string))
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Listens to downloader status. Enables or disables the wake lock depending on the status.
|
* Listens to downloader status. Enables or disables the wake lock depending on the status.
|
||||||
*/
|
*/
|
||||||
|
@ -157,6 +157,11 @@ class Downloader(
|
|||||||
notifier.paused = true
|
notifier.paused = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if downloader is paused
|
||||||
|
*/
|
||||||
|
fun isPaused() = !isRunning
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes everything from the queue.
|
* Removes everything from the queue.
|
||||||
*
|
*
|
||||||
|
@ -1,60 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.glide
|
|
||||||
|
|
||||||
import android.content.ContentValues.TAG
|
|
||||||
import android.util.Log
|
|
||||||
import com.bumptech.glide.Priority
|
|
||||||
import com.bumptech.glide.load.DataSource
|
|
||||||
import com.bumptech.glide.load.data.DataFetcher
|
|
||||||
import timber.log.Timber
|
|
||||||
import java.io.File
|
|
||||||
import java.io.FileInputStream
|
|
||||||
import java.io.FileNotFoundException
|
|
||||||
import java.io.IOException
|
|
||||||
import java.io.InputStream
|
|
||||||
|
|
||||||
open class FileFetcher(private val filePath: String = "") : DataFetcher<InputStream> {
|
|
||||||
|
|
||||||
private var data: InputStream? = null
|
|
||||||
|
|
||||||
override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in InputStream>) {
|
|
||||||
loadFromFile(callback)
|
|
||||||
}
|
|
||||||
|
|
||||||
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)) {
|
|
||||||
Timber.d(e, "Failed to open file")
|
|
||||||
}
|
|
||||||
callback.onLoadFailed(e)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
callback.onDataReady(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun cleanup() {
|
|
||||||
try {
|
|
||||||
data?.close()
|
|
||||||
} catch (e: IOException) {
|
|
||||||
// Ignored.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun cancel() {
|
|
||||||
// Do nothing.
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getDataClass(): Class<InputStream> {
|
|
||||||
return InputStream::class.java
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getDataSource(): DataSource {
|
|
||||||
return DataSource.LOCAL
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,25 +0,0 @@
|
|||||||
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() }
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,86 +0,0 @@
|
|||||||
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
|
|
||||||
import java.io.InputStream
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A [DataFetcher] for loading a cover of a library manga.
|
|
||||||
* It tries to load the cover from our custom cache, and if it's not found, it fallbacks to network
|
|
||||||
* and copies the result to the cache.
|
|
||||||
*
|
|
||||||
* @param networkFetcher the network fetcher for this cover.
|
|
||||||
* @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 coverCache: CoverCache
|
|
||||||
) : LibraryMangaCustomCoverFetcher(manga, coverCache) {
|
|
||||||
|
|
||||||
override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in InputStream>) {
|
|
||||||
getCustomCoverFile()?.let {
|
|
||||||
loadFromFile(it, callback)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
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"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onLoadFailed(e: Exception) {
|
|
||||||
callback.onLoadFailed(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
loadFromFile(cover, callback)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun cleanup() {
|
|
||||||
super.cleanup()
|
|
||||||
networkFetcher.cleanup()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun cancel() {
|
|
||||||
super.cancel()
|
|
||||||
networkFetcher.cancel()
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,15 +0,0 @@
|
|||||||
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,134 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.glide
|
|
||||||
|
|
||||||
import com.bumptech.glide.integration.okhttp3.OkHttpStreamFetcher
|
|
||||||
import com.bumptech.glide.load.Options
|
|
||||||
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 uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
import java.io.InputStream
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A class for loading a cover associated with a [Manga] that can be present in our own cache.
|
|
||||||
* Coupled with [LibraryMangaUrlFetcher], this class allows to implement the following flow:
|
|
||||||
*
|
|
||||||
* - Check in RAM LRU.
|
|
||||||
* - Check in disk LRU.
|
|
||||||
* - Check in this module.
|
|
||||||
* - Fetch from the network connection.
|
|
||||||
*
|
|
||||||
* @param context the application context.
|
|
||||||
*/
|
|
||||||
class MangaThumbnailModelLoader : ModelLoader<MangaThumbnail, InputStream> {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cover cache where persistent covers are stored.
|
|
||||||
*/
|
|
||||||
private val coverCache: CoverCache by injectLazy()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Source manager.
|
|
||||||
*/
|
|
||||||
private val sourceManager: SourceManager by injectLazy()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Default network client.
|
|
||||||
*/
|
|
||||||
private val defaultClient = Injekt.get<NetworkHelper>().client
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Map where request headers are stored for a source.
|
|
||||||
*/
|
|
||||||
private val cachedHeaders = hashMapOf<Long, LazyHeaders>()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Factory class for creating [MangaThumbnailModelLoader] instances.
|
|
||||||
*/
|
|
||||||
class Factory : ModelLoaderFactory<MangaThumbnail, InputStream> {
|
|
||||||
|
|
||||||
override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<MangaThumbnail, InputStream> {
|
|
||||||
return MangaThumbnailModelLoader()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun teardown() {}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun handles(model: MangaThumbnail): Boolean {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a fetcher for the given manga or null if the url is empty.
|
|
||||||
*
|
|
||||||
* @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(
|
|
||||||
mangaThumbnail: MangaThumbnail,
|
|
||||||
width: Int,
|
|
||||||
height: Int,
|
|
||||||
options: Options
|
|
||||||
): ModelLoader.LoadData<InputStream>? {
|
|
||||||
val manga = mangaThumbnail.manga
|
|
||||||
val url = manga.thumbnail_url
|
|
||||||
|
|
||||||
if (url.isNullOrEmpty()) {
|
|
||||||
return if (!manga.favorite || manga.isLocal()) {
|
|
||||||
null
|
|
||||||
} else {
|
|
||||||
ModelLoader.LoadData(mangaThumbnail, LibraryMangaCustomCoverFetcher(manga, coverCache))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (url.startsWith("http", true)) {
|
|
||||||
val source = sourceManager.get(manga.source) as? HttpSource
|
|
||||||
val glideUrl = GlideUrl(url, getHeaders(manga, source))
|
|
||||||
|
|
||||||
// Get the resource fetcher for this request url.
|
|
||||||
val networkFetcher = OkHttpStreamFetcher(source?.client ?: defaultClient, glideUrl)
|
|
||||||
|
|
||||||
if (!manga.favorite) {
|
|
||||||
return ModelLoader.LoadData(glideUrl, networkFetcher)
|
|
||||||
}
|
|
||||||
|
|
||||||
val libraryFetcher = LibraryMangaUrlFetcher(networkFetcher, manga, coverCache)
|
|
||||||
|
|
||||||
// Return an instance of the fetcher providing the needed elements.
|
|
||||||
return ModelLoader.LoadData(mangaThumbnail, libraryFetcher)
|
|
||||||
} else {
|
|
||||||
// Return an instance of the fetcher providing the needed elements.
|
|
||||||
return ModelLoader.LoadData(mangaThumbnail, FileFetcher(url.removePrefix("file://")))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the request headers for a source copying its OkHttp headers and caching them.
|
|
||||||
*
|
|
||||||
* @param manga the model.
|
|
||||||
*/
|
|
||||||
private fun getHeaders(manga: Manga, source: HttpSource?): Headers {
|
|
||||||
if (source == null) return LazyHeaders.DEFAULT
|
|
||||||
|
|
||||||
return cachedHeaders.getOrPut(manga.source) {
|
|
||||||
LazyHeaders.Builder().apply {
|
|
||||||
val nullStr: String? = null
|
|
||||||
setHeader("User-Agent", nullStr)
|
|
||||||
for ((key, value) in source.headers.toMultimap()) {
|
|
||||||
addHeader(key, value[0])
|
|
||||||
}
|
|
||||||
}.build()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,72 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.glide
|
|
||||||
|
|
||||||
import com.bumptech.glide.Priority
|
|
||||||
import com.bumptech.glide.load.DataSource
|
|
||||||
import com.bumptech.glide.load.Options
|
|
||||||
import com.bumptech.glide.load.data.DataFetcher
|
|
||||||
import com.bumptech.glide.load.model.ModelLoader
|
|
||||||
import com.bumptech.glide.load.model.ModelLoaderFactory
|
|
||||||
import com.bumptech.glide.load.model.MultiModelLoaderFactory
|
|
||||||
import com.bumptech.glide.signature.ObjectKey
|
|
||||||
import java.io.IOException
|
|
||||||
import java.io.InputStream
|
|
||||||
|
|
||||||
class PassthroughModelLoader : ModelLoader<InputStream, InputStream> {
|
|
||||||
|
|
||||||
override fun buildLoadData(
|
|
||||||
model: InputStream,
|
|
||||||
width: Int,
|
|
||||||
height: Int,
|
|
||||||
options: Options
|
|
||||||
): ModelLoader.LoadData<InputStream>? {
|
|
||||||
return ModelLoader.LoadData(ObjectKey(model), Fetcher(model))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun handles(model: InputStream): Boolean {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
class Fetcher(private val stream: InputStream) : DataFetcher<InputStream> {
|
|
||||||
|
|
||||||
override fun getDataClass(): Class<InputStream> {
|
|
||||||
return InputStream::class.java
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun cleanup() {
|
|
||||||
try {
|
|
||||||
stream.close()
|
|
||||||
} catch (e: IOException) {
|
|
||||||
// Do nothing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getDataSource(): DataSource {
|
|
||||||
return DataSource.LOCAL
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun cancel() {
|
|
||||||
// Do nothing
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun loadData(
|
|
||||||
priority: Priority,
|
|
||||||
callback: DataFetcher.DataCallback<in InputStream>
|
|
||||||
) {
|
|
||||||
callback.onDataReady(stream)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Factory class for creating [PassthroughModelLoader] instances.
|
|
||||||
*/
|
|
||||||
class Factory : ModelLoaderFactory<InputStream, InputStream> {
|
|
||||||
|
|
||||||
override fun build(
|
|
||||||
multiFactory: MultiModelLoaderFactory
|
|
||||||
): ModelLoader<InputStream, InputStream> {
|
|
||||||
return PassthroughModelLoader()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun teardown() {}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,55 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.glide
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.graphics.drawable.Drawable
|
|
||||||
import com.bumptech.glide.Glide
|
|
||||||
import com.bumptech.glide.GlideBuilder
|
|
||||||
import com.bumptech.glide.Registry
|
|
||||||
import com.bumptech.glide.annotation.GlideModule
|
|
||||||
import com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader
|
|
||||||
import com.bumptech.glide.load.DecodeFormat
|
|
||||||
import com.bumptech.glide.load.engine.cache.InternalCacheDiskCacheFactory
|
|
||||||
import com.bumptech.glide.load.model.GlideUrl
|
|
||||||
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
|
|
||||||
import com.bumptech.glide.module.AppGlideModule
|
|
||||||
import com.bumptech.glide.request.RequestOptions
|
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
import java.io.InputStream
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class used to update Glide module settings
|
|
||||||
*/
|
|
||||||
@GlideModule
|
|
||||||
class TachiGlideModule : AppGlideModule() {
|
|
||||||
|
|
||||||
override fun applyOptions(context: Context, builder: GlideBuilder) {
|
|
||||||
builder.setDiskCache(InternalCacheDiskCacheFactory(context, 50 * 1024 * 1024))
|
|
||||||
builder.setDefaultRequestOptions(RequestOptions().format(DecodeFormat.PREFER_RGB_565))
|
|
||||||
builder.setDefaultTransitionOptions(
|
|
||||||
Drawable::class.java,
|
|
||||||
DrawableTransitionOptions.withCrossFade()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
|
|
||||||
val networkFactory = OkHttpUrlLoader.Factory(Injekt.get<NetworkHelper>().client)
|
|
||||||
|
|
||||||
registry.replace(
|
|
||||||
GlideUrl::class.java,
|
|
||||||
InputStream::class.java,
|
|
||||||
networkFactory
|
|
||||||
)
|
|
||||||
registry.append(
|
|
||||||
MangaThumbnail::class.java,
|
|
||||||
InputStream::class.java,
|
|
||||||
MangaThumbnailModelLoader.Factory()
|
|
||||||
)
|
|
||||||
registry.append(
|
|
||||||
InputStream::class.java,
|
|
||||||
InputStream::class.java,
|
|
||||||
PassthroughModelLoader.Factory()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
@ -8,7 +8,9 @@ import androidx.work.PeriodicWorkRequestBuilder
|
|||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
import androidx.work.Worker
|
import androidx.work.Worker
|
||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
|
import eu.kanade.tachiyomi.data.preference.CHARGING
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
|
import eu.kanade.tachiyomi.data.preference.UNMETERED_NETWORK
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
@ -31,9 +33,9 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
|||||||
val preferences = Injekt.get<PreferencesHelper>()
|
val preferences = Injekt.get<PreferencesHelper>()
|
||||||
val interval = prefInterval ?: preferences.libraryUpdateInterval().get()
|
val interval = prefInterval ?: preferences.libraryUpdateInterval().get()
|
||||||
if (interval > 0) {
|
if (interval > 0) {
|
||||||
val restrictions = preferences.libraryUpdateRestriction()!!
|
val restrictions = preferences.libraryUpdateRestriction().get()
|
||||||
val acRestriction = "ac" in restrictions
|
val acRestriction = CHARGING in restrictions
|
||||||
val wifiRestriction = if ("wifi" in restrictions) {
|
val wifiRestriction = if (UNMETERED_NETWORK in restrictions) {
|
||||||
NetworkType.UNMETERED
|
NetworkType.UNMETERED
|
||||||
} else {
|
} else {
|
||||||
NetworkType.CONNECTED
|
NetworkType.CONNECTED
|
||||||
|
@ -6,19 +6,22 @@ import android.content.Context
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
|
import android.graphics.drawable.BitmapDrawable
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.app.NotificationManagerCompat
|
import androidx.core.app.NotificationManagerCompat
|
||||||
import com.bumptech.glide.Glide
|
import coil.imageLoader
|
||||||
|
import coil.request.ImageRequest
|
||||||
|
import coil.transform.CircleCropTransformation
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.glide.toMangaThumbnail
|
|
||||||
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
||||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||||
import eu.kanade.tachiyomi.util.lang.chop
|
import eu.kanade.tachiyomi.util.lang.chop
|
||||||
|
import eu.kanade.tachiyomi.util.lang.launchUI
|
||||||
import eu.kanade.tachiyomi.util.system.notification
|
import eu.kanade.tachiyomi.util.system.notification
|
||||||
import eu.kanade.tachiyomi.util.system.notificationBuilder
|
import eu.kanade.tachiyomi.util.system.notificationBuilder
|
||||||
import eu.kanade.tachiyomi.util.system.notificationManager
|
import eu.kanade.tachiyomi.util.system.notificationManager
|
||||||
@ -48,7 +51,7 @@ class LibraryUpdateNotifier(private val context: Context) {
|
|||||||
* Cached progress notification to avoid creating a lot.
|
* Cached progress notification to avoid creating a lot.
|
||||||
*/
|
*/
|
||||||
val progressNotificationBuilder by lazy {
|
val progressNotificationBuilder by lazy {
|
||||||
context.notificationBuilder(Notifications.CHANNEL_LIBRARY) {
|
context.notificationBuilder(Notifications.CHANNEL_LIBRARY_PROGRESS) {
|
||||||
setContentTitle(context.getString(R.string.app_name))
|
setContentTitle(context.getString(R.string.app_name))
|
||||||
setSmallIcon(R.drawable.ic_refresh_24dp)
|
setSmallIcon(R.drawable.ic_refresh_24dp)
|
||||||
setLargeIcon(notificationBitmap)
|
setLargeIcon(notificationBitmap)
|
||||||
@ -61,21 +64,25 @@ class LibraryUpdateNotifier(private val context: Context) {
|
|||||||
/**
|
/**
|
||||||
* Shows the notification containing the currently updating manga and the progress.
|
* Shows the notification containing the currently updating manga and the progress.
|
||||||
*
|
*
|
||||||
* @param manga the manga that's being updated.
|
* @param manga the manga that are being updated.
|
||||||
* @param current the current progress.
|
* @param current the current progress.
|
||||||
* @param total the total progress.
|
* @param total the total progress.
|
||||||
*/
|
*/
|
||||||
fun showProgressNotification(manga: Manga, current: Int, total: Int) {
|
fun showProgressNotification(manga: List<Manga>, current: Int, total: Int) {
|
||||||
val title = if (preferences.hideNotificationContent()) {
|
if (preferences.hideNotificationContent()) {
|
||||||
context.getString(R.string.notification_check_updates)
|
progressNotificationBuilder
|
||||||
|
.setContentTitle(context.getString(R.string.notification_check_updates))
|
||||||
|
.setContentText("($current/$total)")
|
||||||
} else {
|
} else {
|
||||||
manga.title
|
val updatingText = manga.joinToString("\n") { it.title.chop(40) }
|
||||||
|
progressNotificationBuilder
|
||||||
|
.setContentTitle(context.getString(R.string.notification_updating, current, total))
|
||||||
|
.setStyle(NotificationCompat.BigTextStyle().bigText(updatingText))
|
||||||
}
|
}
|
||||||
|
|
||||||
context.notificationManager.notify(
|
context.notificationManager.notify(
|
||||||
Notifications.ID_LIBRARY_PROGRESS,
|
Notifications.ID_LIBRARY_PROGRESS,
|
||||||
progressNotificationBuilder
|
progressNotificationBuilder
|
||||||
.setContentTitle(title)
|
|
||||||
.setProgress(total, current, false)
|
.setProgress(total, current, false)
|
||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
@ -94,7 +101,7 @@ class LibraryUpdateNotifier(private val context: Context) {
|
|||||||
|
|
||||||
context.notificationManager.notify(
|
context.notificationManager.notify(
|
||||||
Notifications.ID_LIBRARY_ERROR,
|
Notifications.ID_LIBRARY_ERROR,
|
||||||
context.notificationBuilder(Notifications.CHANNEL_LIBRARY) {
|
context.notificationBuilder(Notifications.CHANNEL_LIBRARY_ERROR) {
|
||||||
setContentTitle(context.resources.getQuantityString(R.plurals.notification_update_error, errors.size, errors.size))
|
setContentTitle(context.resources.getQuantityString(R.plurals.notification_update_error, errors.size, errors.size))
|
||||||
setStyle(
|
setStyle(
|
||||||
NotificationCompat.BigTextStyle().bigText(
|
NotificationCompat.BigTextStyle().bigText(
|
||||||
@ -110,7 +117,7 @@ class LibraryUpdateNotifier(private val context: Context) {
|
|||||||
setContentIntent(errorLogIntent)
|
setContentIntent(errorLogIntent)
|
||||||
addAction(
|
addAction(
|
||||||
R.drawable.ic_folder_24dp,
|
R.drawable.ic_folder_24dp,
|
||||||
context.getString(R.string.action_open_log),
|
context.getString(R.string.action_show_errors),
|
||||||
errorLogIntent
|
errorLogIntent
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -165,14 +172,17 @@ class LibraryUpdateNotifier(private val context: Context) {
|
|||||||
|
|
||||||
// Per-manga notification
|
// Per-manga notification
|
||||||
if (!preferences.hideNotificationContent()) {
|
if (!preferences.hideNotificationContent()) {
|
||||||
updates.forEach { (manga, chapters) ->
|
launchUI {
|
||||||
notify(manga.id.hashCode(), createNewChaptersNotification(manga, chapters))
|
updates.forEach { (manga, chapters) ->
|
||||||
|
notify(manga.id.hashCode(), createNewChaptersNotification(manga, chapters))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createNewChaptersNotification(manga: Manga, chapters: Array<Chapter>): Notification {
|
private suspend fun createNewChaptersNotification(manga: Manga, chapters: Array<Chapter>): Notification {
|
||||||
|
val icon = getMangaIcon(manga)
|
||||||
return context.notification(Notifications.CHANNEL_NEW_CHAPTERS) {
|
return context.notification(Notifications.CHANNEL_NEW_CHAPTERS) {
|
||||||
setContentTitle(manga.title)
|
setContentTitle(manga.title)
|
||||||
|
|
||||||
@ -182,7 +192,6 @@ class LibraryUpdateNotifier(private val context: Context) {
|
|||||||
|
|
||||||
setSmallIcon(R.drawable.ic_tachi)
|
setSmallIcon(R.drawable.ic_tachi)
|
||||||
|
|
||||||
val icon = getMangaIcon(manga)
|
|
||||||
if (icon != null) {
|
if (icon != null) {
|
||||||
setLargeIcon(icon)
|
setLargeIcon(icon)
|
||||||
}
|
}
|
||||||
@ -197,7 +206,7 @@ class LibraryUpdateNotifier(private val context: Context) {
|
|||||||
|
|
||||||
// Mark chapters as read action
|
// Mark chapters as read action
|
||||||
addAction(
|
addAction(
|
||||||
R.drawable.ic_glasses_black_24dp,
|
R.drawable.ic_glasses_24dp,
|
||||||
context.getString(R.string.action_mark_as_read),
|
context.getString(R.string.action_mark_as_read),
|
||||||
NotificationReceiver.markAsReadPendingBroadcast(
|
NotificationReceiver.markAsReadPendingBroadcast(
|
||||||
context,
|
context,
|
||||||
@ -226,23 +235,14 @@ class LibraryUpdateNotifier(private val context: Context) {
|
|||||||
context.notificationManager.cancel(Notifications.ID_LIBRARY_PROGRESS)
|
context.notificationManager.cancel(Notifications.ID_LIBRARY_PROGRESS)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getMangaIcon(manga: Manga): Bitmap? {
|
private suspend fun getMangaIcon(manga: Manga): Bitmap? {
|
||||||
return try {
|
val request = ImageRequest.Builder(context)
|
||||||
Glide.with(context)
|
.data(manga)
|
||||||
.asBitmap()
|
.transformations(CircleCropTransformation())
|
||||||
.load(manga.toMangaThumbnail())
|
.size(NOTIF_ICON_SIZE)
|
||||||
.dontTransform()
|
.build()
|
||||||
.centerCrop()
|
val drawable = context.imageLoader.execute(request).drawable
|
||||||
.circleCrop()
|
return (drawable as? BitmapDrawable)?.bitmap
|
||||||
.override(
|
|
||||||
NOTIF_ICON_SIZE,
|
|
||||||
NOTIF_ICON_SIZE
|
|
||||||
)
|
|
||||||
.submit()
|
|
||||||
.get()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getNewChaptersDescription(chapters: Array<Chapter>): String {
|
private fun getNewChaptersDescription(chapters: Array<Chapter>): String {
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
package eu.kanade.tachiyomi.data.library
|
package eu.kanade.tachiyomi.data.library
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import java.util.Collections
|
||||||
|
import kotlin.math.abs
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class will provide various functions to rank manga to efficiently schedule manga to update.
|
* This class will provide various functions to rank manga to efficiently schedule manga to update.
|
||||||
@ -9,9 +11,26 @@ object LibraryUpdateRanker {
|
|||||||
|
|
||||||
val rankingScheme = listOf(
|
val rankingScheme = listOf(
|
||||||
(this::lexicographicRanking)(),
|
(this::lexicographicRanking)(),
|
||||||
(this::latestFirstRanking)()
|
(this::latestFirstRanking)(),
|
||||||
|
(this::nextFirstRanking)()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides a total ordering over all the Mangas.
|
||||||
|
*
|
||||||
|
* Orders the manga based on the distance between the next expected update and now.
|
||||||
|
* The comparator is reversed, placing the smallest (and thus closest to updating now) first.
|
||||||
|
*/
|
||||||
|
fun nextFirstRanking(): Comparator<Manga> {
|
||||||
|
val time = System.currentTimeMillis()
|
||||||
|
return Collections.reverseOrder(
|
||||||
|
Comparator { mangaFirst: Manga,
|
||||||
|
mangaSecond: Manga ->
|
||||||
|
compareValues(abs(mangaSecond.next_update - time), abs(mangaFirst.next_update - time))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides a total ordering over all the [Manga]s.
|
* Provides a total ordering over all the [Manga]s.
|
||||||
*
|
*
|
||||||
|
@ -20,13 +20,17 @@ import eu.kanade.tachiyomi.data.library.LibraryUpdateRanker.rankingScheme
|
|||||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateService.Companion.start
|
import eu.kanade.tachiyomi.data.library.LibraryUpdateService.Companion.start
|
||||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
|
import eu.kanade.tachiyomi.data.track.EnhancedTrackService
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
|
import eu.kanade.tachiyomi.data.track.TrackService
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.model.toSChapter
|
import eu.kanade.tachiyomi.source.model.toSChapter
|
||||||
import eu.kanade.tachiyomi.source.model.toSManga
|
import eu.kanade.tachiyomi.source.model.toSManga
|
||||||
import eu.kanade.tachiyomi.util.chapter.NoChaptersException
|
import eu.kanade.tachiyomi.util.chapter.NoChaptersException
|
||||||
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
|
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
|
||||||
|
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithTrackServiceTwoWay
|
||||||
|
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||||
import eu.kanade.tachiyomi.util.prepUpdateCover
|
import eu.kanade.tachiyomi.util.prepUpdateCover
|
||||||
import eu.kanade.tachiyomi.util.shouldDownloadNewChapters
|
import eu.kanade.tachiyomi.util.shouldDownloadNewChapters
|
||||||
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
||||||
@ -44,10 +48,14 @@ import kotlinx.coroutines.awaitAll
|
|||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.supervisorScope
|
import kotlinx.coroutines.supervisorScope
|
||||||
|
import kotlinx.coroutines.sync.Semaphore
|
||||||
|
import kotlinx.coroutines.sync.withPermit
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.util.concurrent.CopyOnWriteArrayList
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
import java.util.concurrent.atomic.AtomicInteger
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -71,6 +79,7 @@ class LibraryUpdateService(
|
|||||||
private lateinit var notifier: LibraryUpdateNotifier
|
private lateinit var notifier: LibraryUpdateNotifier
|
||||||
private lateinit var ioScope: CoroutineScope
|
private lateinit var ioScope: CoroutineScope
|
||||||
|
|
||||||
|
private var mangaToUpdate: List<LibraryManga> = mutableListOf()
|
||||||
private var updateJob: Job? = null
|
private var updateJob: Job? = null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -86,6 +95,8 @@ class LibraryUpdateService(
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
|
private var instance: LibraryUpdateService? = null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Key for category to update.
|
* Key for category to update.
|
||||||
*/
|
*/
|
||||||
@ -116,17 +127,18 @@ class LibraryUpdateService(
|
|||||||
* @return true if service newly started, false otherwise
|
* @return true if service newly started, false otherwise
|
||||||
*/
|
*/
|
||||||
fun start(context: Context, category: Category? = null, target: Target = Target.CHAPTERS): Boolean {
|
fun start(context: Context, category: Category? = null, target: Target = Target.CHAPTERS): Boolean {
|
||||||
if (!isRunning(context)) {
|
return if (!isRunning(context)) {
|
||||||
val intent = Intent(context, LibraryUpdateService::class.java).apply {
|
val intent = Intent(context, LibraryUpdateService::class.java).apply {
|
||||||
putExtra(KEY_TARGET, target)
|
putExtra(KEY_TARGET, target)
|
||||||
category?.let { putExtra(KEY_CATEGORY, it.id) }
|
category?.let { putExtra(KEY_CATEGORY, it.id) }
|
||||||
}
|
}
|
||||||
ContextCompat.startForegroundService(context, intent)
|
ContextCompat.startForegroundService(context, intent)
|
||||||
|
|
||||||
return true
|
true
|
||||||
|
} else {
|
||||||
|
instance?.addMangaToQueue(category?.id ?: -1, target)
|
||||||
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -163,6 +175,9 @@ class LibraryUpdateService(
|
|||||||
if (wakeLock.isHeld) {
|
if (wakeLock.isHeld) {
|
||||||
wakeLock.release()
|
wakeLock.release()
|
||||||
}
|
}
|
||||||
|
if (instance == this) {
|
||||||
|
instance = null
|
||||||
|
}
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -186,23 +201,25 @@ class LibraryUpdateService(
|
|||||||
val target = intent.getSerializableExtra(KEY_TARGET) as? Target
|
val target = intent.getSerializableExtra(KEY_TARGET) as? Target
|
||||||
?: return START_NOT_STICKY
|
?: return START_NOT_STICKY
|
||||||
|
|
||||||
// Unsubscribe from any previous subscription if needed.
|
instance = this
|
||||||
|
|
||||||
|
// Unsubscribe from any previous subscription if needed
|
||||||
updateJob?.cancel()
|
updateJob?.cancel()
|
||||||
|
|
||||||
// Update favorite manga. Destroy service when completed or in case of an error.
|
// Update favorite manga
|
||||||
val selectedScheme = preferences.libraryUpdatePrioritization().get()
|
val categoryId = intent.getIntExtra(KEY_CATEGORY, -1)
|
||||||
val mangaList = getMangaToUpdate(intent, target)
|
addMangaToQueue(categoryId, target)
|
||||||
.sortedWith(rankingScheme[selectedScheme])
|
|
||||||
|
|
||||||
|
// Destroy service when completed or in case of an error.
|
||||||
val handler = CoroutineExceptionHandler { _, exception ->
|
val handler = CoroutineExceptionHandler { _, exception ->
|
||||||
Timber.e(exception)
|
Timber.e(exception)
|
||||||
stopSelf(startId)
|
stopSelf(startId)
|
||||||
}
|
}
|
||||||
updateJob = ioScope.launch(handler) {
|
updateJob = ioScope.launch(handler) {
|
||||||
when (target) {
|
when (target) {
|
||||||
Target.CHAPTERS -> updateChapterList(mangaList)
|
Target.CHAPTERS -> updateChapterList()
|
||||||
Target.COVERS -> updateCovers(mangaList)
|
Target.COVERS -> updateCovers()
|
||||||
Target.TRACKING -> updateTrackings(mangaList)
|
Target.TRACKING -> updateTrackings()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
updateJob?.invokeOnCompletion { stopSelf(startId) }
|
updateJob?.invokeOnCompletion { stopSelf(startId) }
|
||||||
@ -211,32 +228,41 @@ class LibraryUpdateService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the list of manga to be updated.
|
* Adds list of manga to be updated.
|
||||||
*
|
*
|
||||||
* @param intent the update intent.
|
* @param category the ID of the category to update, or -1 if no category specified.
|
||||||
* @param target the target to update.
|
* @param target the target to update.
|
||||||
* @return a list of manga to update
|
|
||||||
*/
|
*/
|
||||||
fun getMangaToUpdate(intent: Intent, target: Target): List<LibraryManga> {
|
fun addMangaToQueue(categoryId: Int, target: Target) {
|
||||||
val categoryId = intent.getIntExtra(KEY_CATEGORY, -1)
|
val libraryManga = db.getLibraryMangas().executeAsBlocking()
|
||||||
|
|
||||||
var listToUpdate = if (categoryId != -1) {
|
var listToUpdate = if (categoryId != -1) {
|
||||||
db.getLibraryMangas().executeAsBlocking().filter { it.category == categoryId }
|
libraryManga.filter { it.category == categoryId }
|
||||||
} else {
|
} else {
|
||||||
val categoriesToUpdate = preferences.libraryUpdateCategories().get().map(String::toInt)
|
val categoriesToUpdate = preferences.libraryUpdateCategories().get().map(String::toInt)
|
||||||
if (categoriesToUpdate.isNotEmpty()) {
|
val listToInclude = if (categoriesToUpdate.isNotEmpty()) {
|
||||||
db.getLibraryMangas().executeAsBlocking()
|
libraryManga.filter { it.category in categoriesToUpdate }
|
||||||
.filter { it.category in categoriesToUpdate }
|
|
||||||
.distinctBy { it.id }
|
|
||||||
} else {
|
} else {
|
||||||
db.getLibraryMangas().executeAsBlocking().distinctBy { it.id }
|
libraryManga
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val categoriesToExclude = preferences.libraryUpdateCategoriesExclude().get().map(String::toInt)
|
||||||
|
val listToExclude = if (categoriesToExclude.isNotEmpty()) {
|
||||||
|
libraryManga.filter { it.category in categoriesToExclude }
|
||||||
|
} else {
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
listToInclude.minus(listToExclude)
|
||||||
}
|
}
|
||||||
if (target == Target.CHAPTERS && preferences.updateOnlyNonCompleted()) {
|
if (target == Target.CHAPTERS && preferences.updateOnlyNonCompleted()) {
|
||||||
listToUpdate = listToUpdate.filter { it.status != SManga.COMPLETED }
|
listToUpdate = listToUpdate.filterNot { it.status == SManga.COMPLETED }
|
||||||
}
|
}
|
||||||
|
|
||||||
return listToUpdate
|
val selectedScheme = preferences.libraryUpdatePrioritization().get()
|
||||||
|
mangaToUpdate = listToUpdate
|
||||||
|
.distinctBy { it.id }
|
||||||
|
.sortedWith(rankingScheme[selectedScheme])
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -248,51 +274,83 @@ class LibraryUpdateService(
|
|||||||
* @param mangaToUpdate the list to update
|
* @param mangaToUpdate the list to update
|
||||||
* @return an observable delivering the progress of each update.
|
* @return an observable delivering the progress of each update.
|
||||||
*/
|
*/
|
||||||
suspend fun updateChapterList(mangaToUpdate: List<LibraryManga>) {
|
suspend fun updateChapterList() {
|
||||||
|
val semaphore = Semaphore(5)
|
||||||
val progressCount = AtomicInteger(0)
|
val progressCount = AtomicInteger(0)
|
||||||
val newUpdates = mutableListOf<Pair<LibraryManga, Array<Chapter>>>()
|
val currentlyUpdatingManga = CopyOnWriteArrayList<LibraryManga>()
|
||||||
val failedUpdates = mutableListOf<Pair<Manga, String?>>()
|
val newUpdates = CopyOnWriteArrayList<Pair<LibraryManga, Array<Chapter>>>()
|
||||||
var hasDownloads = false
|
val failedUpdates = CopyOnWriteArrayList<Pair<Manga, String?>>()
|
||||||
|
val hasDownloads = AtomicBoolean(false)
|
||||||
|
val loggedServices by lazy { trackManager.services.filter { it.isLogged } }
|
||||||
|
|
||||||
mangaToUpdate.forEach { manga ->
|
withIOContext {
|
||||||
if (updateJob?.isActive != true) {
|
mangaToUpdate.groupBy { it.source }
|
||||||
return
|
.values
|
||||||
}
|
.map { mangaInSource ->
|
||||||
|
async {
|
||||||
|
semaphore.withPermit {
|
||||||
|
mangaInSource.forEach { manga ->
|
||||||
|
if (updateJob?.isActive != true) {
|
||||||
|
return@async
|
||||||
|
}
|
||||||
|
|
||||||
notifier.showProgressNotification(manga, progressCount.andIncrement, mangaToUpdate.size)
|
withUpdateNotification(
|
||||||
|
currentlyUpdatingManga,
|
||||||
|
progressCount,
|
||||||
|
manga,
|
||||||
|
) { manga ->
|
||||||
|
try {
|
||||||
|
val (newChapters, _) = updateManga(manga)
|
||||||
|
|
||||||
try {
|
if (newChapters.isNotEmpty()) {
|
||||||
val (newChapters, _) = updateManga(manga)
|
if (manga.shouldDownloadNewChapters(db, preferences)) {
|
||||||
|
downloadChapters(manga, newChapters)
|
||||||
|
hasDownloads.set(true)
|
||||||
|
}
|
||||||
|
|
||||||
if (newChapters.isNotEmpty()) {
|
// Convert to the manga that contains new chapters
|
||||||
if (manga.shouldDownloadNewChapters(db, preferences)) {
|
newUpdates.add(
|
||||||
downloadChapters(manga, newChapters)
|
manga to newChapters.sortedByDescending { ch -> ch.source_order }
|
||||||
hasDownloads = true
|
.toTypedArray()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
val errorMessage = when (e) {
|
||||||
|
is NoChaptersException -> {
|
||||||
|
getString(R.string.no_chapters_error)
|
||||||
|
}
|
||||||
|
is SourceManager.SourceNotInstalledException -> {
|
||||||
|
// failedUpdates will already have the source, don't need to copy it into the message
|
||||||
|
getString(R.string.loader_not_implemented_error)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
e.message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
failedUpdates.add(manga to errorMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preferences.autoUpdateTrackers()) {
|
||||||
|
updateTrackings(manga, loggedServices)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert to the manga that contains new chapters
|
|
||||||
newUpdates.add(manga to newChapters.sortedByDescending { ch -> ch.source_order }.toTypedArray())
|
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
.awaitAll()
|
||||||
val errorMessage = if (e is NoChaptersException) {
|
|
||||||
getString(R.string.no_chapters_error)
|
|
||||||
} else {
|
|
||||||
e.message
|
|
||||||
}
|
|
||||||
failedUpdates.add(manga to errorMessage)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
notifier.cancelProgressNotification()
|
notifier.cancelProgressNotification()
|
||||||
|
|
||||||
if (newUpdates.isNotEmpty()) {
|
if (newUpdates.isNotEmpty()) {
|
||||||
notifier.showUpdateNotifications(newUpdates)
|
notifier.showUpdateNotifications(newUpdates)
|
||||||
if (hasDownloads) {
|
if (hasDownloads.get()) {
|
||||||
DownloadService.start(this)
|
DownloadService.start(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (preferences.showLibraryUpdateErrors() && failedUpdates.isNotEmpty()) {
|
if (failedUpdates.isNotEmpty()) {
|
||||||
val errorFile = writeErrorFile(failedUpdates)
|
val errorFile = writeErrorFile(failedUpdates)
|
||||||
notifier.showUpdateErrorNotification(
|
notifier.showUpdateErrorNotification(
|
||||||
failedUpdates.map { it.first.title },
|
failedUpdates.map { it.first.title },
|
||||||
@ -342,64 +400,50 @@ class LibraryUpdateService(
|
|||||||
return syncChaptersWithSource(db, chapters, manga, source)
|
return syncChaptersWithSource(db, chapters, manga, source)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun updateCovers(mangaToUpdate: List<LibraryManga>) {
|
private suspend fun updateCovers() {
|
||||||
var progressCount = 0
|
val semaphore = Semaphore(5)
|
||||||
|
val progressCount = AtomicInteger(0)
|
||||||
|
val currentlyUpdatingManga = CopyOnWriteArrayList<LibraryManga>()
|
||||||
|
|
||||||
mangaToUpdate.forEach { manga ->
|
withIOContext {
|
||||||
if (updateJob?.isActive != true) {
|
mangaToUpdate.groupBy { it.source }
|
||||||
return
|
.values
|
||||||
}
|
.map { mangaInSource ->
|
||||||
|
async {
|
||||||
|
semaphore.withPermit {
|
||||||
|
mangaInSource.forEach { manga ->
|
||||||
|
if (updateJob?.isActive != true) {
|
||||||
|
return@async
|
||||||
|
}
|
||||||
|
|
||||||
notifier.showProgressNotification(manga, progressCount++, mangaToUpdate.size)
|
withUpdateNotification(
|
||||||
|
currentlyUpdatingManga,
|
||||||
|
progressCount,
|
||||||
|
manga,
|
||||||
|
) { manga ->
|
||||||
|
sourceManager.get(manga.source)?.let { source ->
|
||||||
|
try {
|
||||||
|
val networkManga =
|
||||||
|
source.getMangaDetails(manga.toMangaInfo())
|
||||||
|
val sManga = networkManga.toSManga()
|
||||||
|
manga.prepUpdateCover(coverCache, sManga, true)
|
||||||
|
sManga.thumbnail_url?.let {
|
||||||
|
manga.thumbnail_url = it
|
||||||
|
db.insertManga(manga).executeAsBlocking()
|
||||||
|
}
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
// Ignore errors and continue
|
||||||
|
Timber.e(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
sourceManager.get(manga.source)?.let { source ->
|
currentlyUpdatingManga.remove(manga)
|
||||||
try {
|
progressCount.andIncrement
|
||||||
val networkManga = source.getMangaDetails(manga.toMangaInfo())
|
notifier.showProgressNotification(
|
||||||
val sManga = networkManga.toSManga()
|
currentlyUpdatingManga,
|
||||||
manga.prepUpdateCover(coverCache, sManga, true)
|
progressCount.get(),
|
||||||
sManga.thumbnail_url?.let {
|
mangaToUpdate.size
|
||||||
manga.thumbnail_url = it
|
)
|
||||||
db.insertManga(manga).executeAsBlocking()
|
|
||||||
}
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
// Ignore errors and continue
|
|
||||||
Timber.e(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
notifier.cancelProgressNotification()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Method that updates the metadata of the connected tracking services. It's called in a
|
|
||||||
* background thread, so it's safe to do heavy operations or network calls here.
|
|
||||||
*/
|
|
||||||
private suspend fun updateTrackings(mangaToUpdate: List<LibraryManga>) {
|
|
||||||
var progressCount = 0
|
|
||||||
val loggedServices = trackManager.services.filter { it.isLogged }
|
|
||||||
|
|
||||||
mangaToUpdate.forEach { manga ->
|
|
||||||
if (updateJob?.isActive != true) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Notify manga that will update.
|
|
||||||
notifier.showProgressNotification(manga, progressCount++, mangaToUpdate.size)
|
|
||||||
|
|
||||||
// Update the tracking details.
|
|
||||||
db.getTracks(manga).executeAsBlocking()
|
|
||||||
.map { track ->
|
|
||||||
supervisorScope {
|
|
||||||
async {
|
|
||||||
val service = trackManager.getService(track.sync_id)
|
|
||||||
if (service != null && service in loggedServices) {
|
|
||||||
try {
|
|
||||||
val updatedTrack = service.refresh(track)
|
|
||||||
db.insertTrack(updatedTrack).executeAsBlocking()
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
// Ignore errors and continue
|
|
||||||
Timber.e(e)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -408,9 +452,89 @@ class LibraryUpdateService(
|
|||||||
.awaitAll()
|
.awaitAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
coverCache.clearMemoryCache()
|
||||||
notifier.cancelProgressNotification()
|
notifier.cancelProgressNotification()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method that updates the metadata of the connected tracking services. It's called in a
|
||||||
|
* background thread, so it's safe to do heavy operations or network calls here.
|
||||||
|
*/
|
||||||
|
private suspend fun updateTrackings() {
|
||||||
|
var progressCount = 0
|
||||||
|
val loggedServices = trackManager.services.filter { it.isLogged }
|
||||||
|
|
||||||
|
mangaToUpdate.forEach { manga ->
|
||||||
|
if (updateJob?.isActive != true) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
notifier.showProgressNotification(listOf(manga), progressCount++, mangaToUpdate.size)
|
||||||
|
|
||||||
|
// Update the tracking details.
|
||||||
|
updateTrackings(manga, loggedServices)
|
||||||
|
}
|
||||||
|
|
||||||
|
notifier.cancelProgressNotification()
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun updateTrackings(manga: LibraryManga, loggedServices: List<TrackService>) {
|
||||||
|
db.getTracks(manga).executeAsBlocking()
|
||||||
|
.map { track ->
|
||||||
|
supervisorScope {
|
||||||
|
async {
|
||||||
|
val service = trackManager.getService(track.sync_id)
|
||||||
|
if (service != null && service in loggedServices) {
|
||||||
|
try {
|
||||||
|
val updatedTrack = service.refresh(track)
|
||||||
|
db.insertTrack(updatedTrack).executeAsBlocking()
|
||||||
|
|
||||||
|
if (service is EnhancedTrackService) {
|
||||||
|
syncChaptersWithTrackServiceTwoWay(db, db.getChapters(manga).executeAsBlocking(), track, service)
|
||||||
|
}
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
// Ignore errors and continue
|
||||||
|
Timber.e(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.awaitAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun withUpdateNotification(
|
||||||
|
updatingManga: CopyOnWriteArrayList<LibraryManga>,
|
||||||
|
completed: AtomicInteger,
|
||||||
|
manga: LibraryManga,
|
||||||
|
block: suspend (LibraryManga) -> Unit,
|
||||||
|
) {
|
||||||
|
if (updateJob?.isActive != true) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
updatingManga.add(manga)
|
||||||
|
notifier.showProgressNotification(
|
||||||
|
updatingManga,
|
||||||
|
completed.get(),
|
||||||
|
mangaToUpdate.size
|
||||||
|
)
|
||||||
|
|
||||||
|
block(manga)
|
||||||
|
|
||||||
|
if (updateJob?.isActive != true) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
updatingManga.remove(manga)
|
||||||
|
completed.andIncrement
|
||||||
|
notifier.showProgressNotification(
|
||||||
|
updatingManga,
|
||||||
|
completed.get(),
|
||||||
|
mangaToUpdate.size
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Writes basic file of update errors to cache dir.
|
* Writes basic file of update errors to cache dir.
|
||||||
*/
|
*/
|
||||||
@ -419,9 +543,19 @@ class LibraryUpdateService(
|
|||||||
if (errors.isNotEmpty()) {
|
if (errors.isNotEmpty()) {
|
||||||
val file = createFileInCacheDir("tachiyomi_update_errors.txt")
|
val file = createFileInCacheDir("tachiyomi_update_errors.txt")
|
||||||
file.bufferedWriter().use { out ->
|
file.bufferedWriter().use { out ->
|
||||||
errors.forEach { (manga, error) ->
|
// Error file format:
|
||||||
val source = sourceManager.getOrStub(manga.source)
|
// ! Error
|
||||||
out.write("${manga.title} ($source): $error\n")
|
// # Source
|
||||||
|
// - Manga
|
||||||
|
errors.groupBy({ it.second }, { it.first }).forEach { (error, mangas) ->
|
||||||
|
out.write("! ${error}\n")
|
||||||
|
mangas.groupBy { it.source }.forEach { (srcId, mangas) ->
|
||||||
|
val source = sourceManager.getOrStub(srcId)
|
||||||
|
out.write(" # $source\n")
|
||||||
|
mangas.forEach {
|
||||||
|
out.write(" - ${it.title}\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return file
|
return file
|
||||||
|
@ -2,12 +2,11 @@ package eu.kanade.tachiyomi.data.notification
|
|||||||
|
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
import android.content.ClipData
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Handler
|
import androidx.core.content.ContextCompat
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupRestoreService
|
import eu.kanade.tachiyomi.data.backup.BackupRestoreService
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
@ -25,6 +24,7 @@ import eu.kanade.tachiyomi.util.lang.launchIO
|
|||||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||||
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
||||||
import eu.kanade.tachiyomi.util.system.notificationManager
|
import eu.kanade.tachiyomi.util.system.notificationManager
|
||||||
|
import eu.kanade.tachiyomi.util.system.toShareIntent
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
@ -58,22 +58,22 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
ACTION_SHARE_IMAGE ->
|
ACTION_SHARE_IMAGE ->
|
||||||
shareImage(
|
shareImage(
|
||||||
context,
|
context,
|
||||||
intent.getStringExtra(EXTRA_FILE_LOCATION),
|
intent.getStringExtra(EXTRA_FILE_LOCATION)!!,
|
||||||
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)
|
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)
|
||||||
)
|
)
|
||||||
// Delete image from path and dismiss notification
|
// Delete image from path and dismiss notification
|
||||||
ACTION_DELETE_IMAGE ->
|
ACTION_DELETE_IMAGE ->
|
||||||
deleteImage(
|
deleteImage(
|
||||||
context,
|
context,
|
||||||
intent.getStringExtra(EXTRA_FILE_LOCATION),
|
intent.getStringExtra(EXTRA_FILE_LOCATION)!!,
|
||||||
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)
|
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)
|
||||||
)
|
)
|
||||||
// Share backup file
|
// Share backup file
|
||||||
ACTION_SHARE_BACKUP ->
|
ACTION_SHARE_BACKUP ->
|
||||||
shareFile(
|
shareFile(
|
||||||
context,
|
context,
|
||||||
intent.getParcelableExtra(EXTRA_URI),
|
intent.getParcelableExtra(EXTRA_URI)!!,
|
||||||
if (intent.getBooleanExtra(EXTRA_IS_LEGACY_BACKUP, false)) "application/json" else "application/octet-stream+gzip",
|
"application/x-protobuf+gzip",
|
||||||
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)
|
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)
|
||||||
)
|
)
|
||||||
ACTION_CANCEL_RESTORE -> cancelRestore(
|
ACTION_CANCEL_RESTORE -> cancelRestore(
|
||||||
@ -106,7 +106,7 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
ACTION_SHARE_CRASH_LOG ->
|
ACTION_SHARE_CRASH_LOG ->
|
||||||
shareFile(
|
shareFile(
|
||||||
context,
|
context,
|
||||||
intent.getParcelableExtra(EXTRA_URI),
|
intent.getParcelableExtra(EXTRA_URI)!!,
|
||||||
"text/plain",
|
"text/plain",
|
||||||
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)
|
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)
|
||||||
)
|
)
|
||||||
@ -130,16 +130,8 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
* @param notificationId id of notification
|
* @param notificationId id of notification
|
||||||
*/
|
*/
|
||||||
private fun shareImage(context: Context, path: String, notificationId: Int) {
|
private fun shareImage(context: Context, path: String, notificationId: Int) {
|
||||||
val intent = Intent(Intent.ACTION_SEND).apply {
|
|
||||||
val uri = File(path).getUriCompat(context)
|
|
||||||
putExtra(Intent.EXTRA_STREAM, uri)
|
|
||||||
clipData = ClipData.newRawUri(null, uri)
|
|
||||||
type = "image/*"
|
|
||||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
|
|
||||||
}
|
|
||||||
dismissNotification(context, notificationId)
|
dismissNotification(context, notificationId)
|
||||||
// Launch share activity
|
context.startActivity(File(path).getUriCompat(context).toShareIntent(context))
|
||||||
context.startActivity(intent)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -150,16 +142,8 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
* @param notificationId id of notification
|
* @param notificationId id of notification
|
||||||
*/
|
*/
|
||||||
private fun shareFile(context: Context, uri: Uri, fileMimeType: String, notificationId: Int) {
|
private fun shareFile(context: Context, uri: Uri, fileMimeType: String, notificationId: Int) {
|
||||||
val sendIntent = Intent(Intent.ACTION_SEND).apply {
|
|
||||||
putExtra(Intent.EXTRA_STREAM, uri)
|
|
||||||
clipData = ClipData.newRawUri(null, uri)
|
|
||||||
type = fileMimeType
|
|
||||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
|
|
||||||
}
|
|
||||||
// Dismiss notification
|
|
||||||
dismissNotification(context, notificationId)
|
dismissNotification(context, notificationId)
|
||||||
// Launch share activity
|
context.startActivity(uri.toShareIntent(context, fileMimeType))
|
||||||
context.startActivity(sendIntent)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -208,7 +192,7 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
*/
|
*/
|
||||||
private fun cancelRestore(context: Context, notificationId: Int) {
|
private fun cancelRestore(context: Context, notificationId: Int) {
|
||||||
BackupRestoreService.stop(context)
|
BackupRestoreService.stop(context)
|
||||||
Handler().post { dismissNotification(context, notificationId) }
|
ContextCompat.getMainExecutor(context).execute { dismissNotification(context, notificationId) }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -219,7 +203,7 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
*/
|
*/
|
||||||
private fun cancelLibraryUpdate(context: Context, notificationId: Int) {
|
private fun cancelLibraryUpdate(context: Context, notificationId: Int) {
|
||||||
LibraryUpdateService.stop(context)
|
LibraryUpdateService.stop(context)
|
||||||
Handler().post { dismissNotification(context, notificationId) }
|
ContextCompat.getMainExecutor(context).execute { dismissNotification(context, notificationId) }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -281,7 +265,6 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
private const val EXTRA_MANGA_ID = "$ID.$NAME.EXTRA_MANGA_ID"
|
private const val EXTRA_MANGA_ID = "$ID.$NAME.EXTRA_MANGA_ID"
|
||||||
private const val EXTRA_CHAPTER_ID = "$ID.$NAME.EXTRA_CHAPTER_ID"
|
private const val EXTRA_CHAPTER_ID = "$ID.$NAME.EXTRA_CHAPTER_ID"
|
||||||
private const val EXTRA_CHAPTER_URL = "$ID.$NAME.EXTRA_CHAPTER_URL"
|
private const val EXTRA_CHAPTER_URL = "$ID.$NAME.EXTRA_CHAPTER_URL"
|
||||||
private const val EXTRA_IS_LEGACY_BACKUP = "$ID.$NAME.EXTRA_IS_LEGACY_BACKUP"
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a [PendingIntent] that resumes the download of a chapter
|
* Returns a [PendingIntent] that resumes the download of a chapter
|
||||||
@ -494,11 +477,10 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
* @param notificationId id of notification
|
* @param notificationId id of notification
|
||||||
* @return [PendingIntent]
|
* @return [PendingIntent]
|
||||||
*/
|
*/
|
||||||
internal fun shareBackupPendingBroadcast(context: Context, uri: Uri, isLegacyFormat: Boolean, notificationId: Int): PendingIntent {
|
internal fun shareBackupPendingBroadcast(context: Context, uri: Uri, notificationId: Int): PendingIntent {
|
||||||
val intent = Intent(context, NotificationReceiver::class.java).apply {
|
val intent = Intent(context, NotificationReceiver::class.java).apply {
|
||||||
action = ACTION_SHARE_BACKUP
|
action = ACTION_SHARE_BACKUP
|
||||||
putExtra(EXTRA_URI, uri)
|
putExtra(EXTRA_URI, uri)
|
||||||
putExtra(EXTRA_IS_LEGACY_BACKUP, isLegacyFormat)
|
|
||||||
putExtra(EXTRA_NOTIFICATION_ID, notificationId)
|
putExtra(EXTRA_NOTIFICATION_ID, notificationId)
|
||||||
}
|
}
|
||||||
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
package eu.kanade.tachiyomi.data.notification
|
package eu.kanade.tachiyomi.data.notification
|
||||||
|
|
||||||
import android.app.NotificationChannel
|
|
||||||
import android.app.NotificationChannelGroup
|
|
||||||
import android.app.NotificationManager
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Build
|
import androidx.core.app.NotificationManagerCompat
|
||||||
|
import androidx.core.app.NotificationManagerCompat.IMPORTANCE_DEFAULT
|
||||||
|
import androidx.core.app.NotificationManagerCompat.IMPORTANCE_HIGH
|
||||||
|
import androidx.core.app.NotificationManagerCompat.IMPORTANCE_LOW
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.util.system.notificationManager
|
import eu.kanade.tachiyomi.util.system.buildNotificationChannel
|
||||||
|
import eu.kanade.tachiyomi.util.system.buildNotificationChannelGroup
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class to manage the basic information of all the notifications used in the app.
|
* Class to manage the basic information of all the notifications used in the app.
|
||||||
@ -23,8 +24,10 @@ object Notifications {
|
|||||||
/**
|
/**
|
||||||
* Notification channel and ids used by the library updater.
|
* Notification channel and ids used by the library updater.
|
||||||
*/
|
*/
|
||||||
const val CHANNEL_LIBRARY = "library_channel"
|
private const val GROUP_LIBRARY = "group_library"
|
||||||
|
const val CHANNEL_LIBRARY_PROGRESS = "library_progress_channel"
|
||||||
const val ID_LIBRARY_PROGRESS = -101
|
const val ID_LIBRARY_PROGRESS = -101
|
||||||
|
const val CHANNEL_LIBRARY_ERROR = "library_errors_channel"
|
||||||
const val ID_LIBRARY_ERROR = -102
|
const val ID_LIBRARY_ERROR = -102
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -50,6 +53,7 @@ object Notifications {
|
|||||||
*/
|
*/
|
||||||
const val CHANNEL_UPDATES_TO_EXTS = "updates_ext_channel"
|
const val CHANNEL_UPDATES_TO_EXTS = "updates_ext_channel"
|
||||||
const val ID_UPDATES_TO_EXTS = -401
|
const val ID_UPDATES_TO_EXTS = -401
|
||||||
|
const val ID_EXTENSION_INSTALLER = -402
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Notification channel and ids used by the backup/restore system.
|
* Notification channel and ids used by the backup/restore system.
|
||||||
@ -68,96 +72,98 @@ object Notifications {
|
|||||||
const val CHANNEL_CRASH_LOGS = "crash_logs_channel"
|
const val CHANNEL_CRASH_LOGS = "crash_logs_channel"
|
||||||
const val ID_CRASH_LOGS = -601
|
const val ID_CRASH_LOGS = -601
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notification channel used for Incognito Mode
|
||||||
|
*/
|
||||||
|
const val CHANNEL_INCOGNITO_MODE = "incognito_mode_channel"
|
||||||
|
const val ID_INCOGNITO_MODE = -701
|
||||||
|
|
||||||
private val deprecatedChannels = listOf(
|
private val deprecatedChannels = listOf(
|
||||||
"downloader_channel",
|
"downloader_channel",
|
||||||
"backup_restore_complete_channel"
|
"backup_restore_complete_channel",
|
||||||
|
"library_channel",
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates the notification channels introduced in Android Oreo.
|
* Creates the notification channels introduced in Android Oreo.
|
||||||
|
* This won't do anything on Android versions that don't support notification channels.
|
||||||
*
|
*
|
||||||
* @param context The application context.
|
* @param context The application context.
|
||||||
*/
|
*/
|
||||||
fun createChannels(context: Context) {
|
fun createChannels(context: Context) {
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
|
val notificationService = NotificationManagerCompat.from(context)
|
||||||
|
|
||||||
listOf(
|
notificationService.createNotificationChannelGroupsCompat(
|
||||||
NotificationChannelGroup(GROUP_BACKUP_RESTORE, context.getString(R.string.group_backup_restore)),
|
listOf(
|
||||||
NotificationChannelGroup(GROUP_DOWNLOADER, context.getString(R.string.group_downloader))
|
buildNotificationChannelGroup(GROUP_BACKUP_RESTORE) {
|
||||||
).forEach(context.notificationManager::createNotificationChannelGroup)
|
setName(context.getString(R.string.label_backup))
|
||||||
|
},
|
||||||
listOf(
|
buildNotificationChannelGroup(GROUP_DOWNLOADER) {
|
||||||
NotificationChannel(
|
setName(context.getString(R.string.download_notifier_downloader_title))
|
||||||
CHANNEL_COMMON,
|
},
|
||||||
context.getString(R.string.channel_common),
|
buildNotificationChannelGroup(GROUP_LIBRARY) {
|
||||||
NotificationManager.IMPORTANCE_LOW
|
setName(context.getString(R.string.label_library))
|
||||||
),
|
},
|
||||||
NotificationChannel(
|
|
||||||
CHANNEL_LIBRARY,
|
|
||||||
context.getString(R.string.channel_library),
|
|
||||||
NotificationManager.IMPORTANCE_LOW
|
|
||||||
).apply {
|
|
||||||
setShowBadge(false)
|
|
||||||
},
|
|
||||||
NotificationChannel(
|
|
||||||
CHANNEL_DOWNLOADER_PROGRESS,
|
|
||||||
context.getString(R.string.channel_progress),
|
|
||||||
NotificationManager.IMPORTANCE_LOW
|
|
||||||
).apply {
|
|
||||||
group = GROUP_DOWNLOADER
|
|
||||||
setShowBadge(false)
|
|
||||||
},
|
|
||||||
NotificationChannel(
|
|
||||||
CHANNEL_DOWNLOADER_COMPLETE,
|
|
||||||
context.getString(R.string.channel_complete),
|
|
||||||
NotificationManager.IMPORTANCE_LOW
|
|
||||||
).apply {
|
|
||||||
group = GROUP_DOWNLOADER
|
|
||||||
setShowBadge(false)
|
|
||||||
},
|
|
||||||
NotificationChannel(
|
|
||||||
CHANNEL_DOWNLOADER_ERROR,
|
|
||||||
context.getString(R.string.channel_errors),
|
|
||||||
NotificationManager.IMPORTANCE_LOW
|
|
||||||
).apply {
|
|
||||||
group = GROUP_DOWNLOADER
|
|
||||||
setShowBadge(false)
|
|
||||||
},
|
|
||||||
NotificationChannel(
|
|
||||||
CHANNEL_NEW_CHAPTERS,
|
|
||||||
context.getString(R.string.channel_new_chapters),
|
|
||||||
NotificationManager.IMPORTANCE_DEFAULT
|
|
||||||
),
|
|
||||||
NotificationChannel(
|
|
||||||
CHANNEL_UPDATES_TO_EXTS,
|
|
||||||
context.getString(R.string.channel_ext_updates),
|
|
||||||
NotificationManager.IMPORTANCE_DEFAULT
|
|
||||||
),
|
|
||||||
NotificationChannel(
|
|
||||||
CHANNEL_BACKUP_RESTORE_PROGRESS,
|
|
||||||
context.getString(R.string.channel_progress),
|
|
||||||
NotificationManager.IMPORTANCE_LOW
|
|
||||||
).apply {
|
|
||||||
group = GROUP_BACKUP_RESTORE
|
|
||||||
setShowBadge(false)
|
|
||||||
},
|
|
||||||
NotificationChannel(
|
|
||||||
CHANNEL_BACKUP_RESTORE_COMPLETE,
|
|
||||||
context.getString(R.string.channel_complete),
|
|
||||||
NotificationManager.IMPORTANCE_HIGH
|
|
||||||
).apply {
|
|
||||||
group = GROUP_BACKUP_RESTORE
|
|
||||||
setShowBadge(false)
|
|
||||||
setSound(null, null)
|
|
||||||
},
|
|
||||||
NotificationChannel(
|
|
||||||
CHANNEL_CRASH_LOGS,
|
|
||||||
context.getString(R.string.channel_crash_logs),
|
|
||||||
NotificationManager.IMPORTANCE_HIGH
|
|
||||||
)
|
)
|
||||||
).forEach(context.notificationManager::createNotificationChannel)
|
)
|
||||||
|
|
||||||
|
notificationService.createNotificationChannelsCompat(
|
||||||
|
listOf(
|
||||||
|
buildNotificationChannel(CHANNEL_COMMON, IMPORTANCE_LOW) {
|
||||||
|
setName(context.getString(R.string.channel_common))
|
||||||
|
},
|
||||||
|
buildNotificationChannel(CHANNEL_LIBRARY_PROGRESS, IMPORTANCE_LOW) {
|
||||||
|
setName(context.getString(R.string.channel_progress))
|
||||||
|
setGroup(GROUP_LIBRARY)
|
||||||
|
setShowBadge(false)
|
||||||
|
},
|
||||||
|
buildNotificationChannel(CHANNEL_LIBRARY_ERROR, IMPORTANCE_LOW) {
|
||||||
|
setName(context.getString(R.string.channel_errors))
|
||||||
|
setGroup(GROUP_LIBRARY)
|
||||||
|
setShowBadge(false)
|
||||||
|
},
|
||||||
|
buildNotificationChannel(CHANNEL_NEW_CHAPTERS, IMPORTANCE_DEFAULT) {
|
||||||
|
setName(context.getString(R.string.channel_new_chapters))
|
||||||
|
},
|
||||||
|
buildNotificationChannel(CHANNEL_DOWNLOADER_PROGRESS, IMPORTANCE_LOW) {
|
||||||
|
setName(context.getString(R.string.channel_progress))
|
||||||
|
setGroup(GROUP_DOWNLOADER)
|
||||||
|
setShowBadge(false)
|
||||||
|
},
|
||||||
|
buildNotificationChannel(CHANNEL_DOWNLOADER_COMPLETE, IMPORTANCE_LOW) {
|
||||||
|
setName(context.getString(R.string.channel_complete))
|
||||||
|
setGroup(GROUP_DOWNLOADER)
|
||||||
|
setShowBadge(false)
|
||||||
|
},
|
||||||
|
buildNotificationChannel(CHANNEL_DOWNLOADER_ERROR, IMPORTANCE_LOW) {
|
||||||
|
setName(context.getString(R.string.channel_errors))
|
||||||
|
setGroup(GROUP_DOWNLOADER)
|
||||||
|
setShowBadge(false)
|
||||||
|
},
|
||||||
|
buildNotificationChannel(CHANNEL_BACKUP_RESTORE_PROGRESS, IMPORTANCE_LOW) {
|
||||||
|
setName(context.getString(R.string.channel_progress))
|
||||||
|
setGroup(GROUP_BACKUP_RESTORE)
|
||||||
|
setShowBadge(false)
|
||||||
|
},
|
||||||
|
buildNotificationChannel(CHANNEL_BACKUP_RESTORE_COMPLETE, IMPORTANCE_HIGH) {
|
||||||
|
setName(context.getString(R.string.channel_complete))
|
||||||
|
setGroup(GROUP_BACKUP_RESTORE)
|
||||||
|
setShowBadge(false)
|
||||||
|
setSound(null, null)
|
||||||
|
},
|
||||||
|
buildNotificationChannel(CHANNEL_CRASH_LOGS, IMPORTANCE_HIGH) {
|
||||||
|
setName(context.getString(R.string.channel_crash_logs))
|
||||||
|
},
|
||||||
|
buildNotificationChannel(CHANNEL_INCOGNITO_MODE, IMPORTANCE_LOW) {
|
||||||
|
setName(context.getString(R.string.pref_incognito_mode))
|
||||||
|
},
|
||||||
|
buildNotificationChannel(CHANNEL_UPDATES_TO_EXTS, IMPORTANCE_DEFAULT) {
|
||||||
|
setName(context.getString(R.string.channel_ext_updates))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
// Delete old notification channels
|
// Delete old notification channels
|
||||||
deprecatedChannels.forEach(context.notificationManager::deleteNotificationChannel)
|
deprecatedChannels.forEach(notificationService::deleteNotificationChannel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,15 +7,15 @@ object PreferenceKeys {
|
|||||||
|
|
||||||
const val themeMode = "pref_theme_mode_key"
|
const val themeMode = "pref_theme_mode_key"
|
||||||
|
|
||||||
const val themeLight = "pref_theme_light_key"
|
const val appTheme = "pref_app_theme"
|
||||||
|
|
||||||
const val themeDark = "pref_theme_dark_key"
|
const val themeDarkAmoled = "pref_theme_dark_amoled_key"
|
||||||
|
|
||||||
const val confirmExit = "pref_confirm_exit"
|
const val confirmExit = "pref_confirm_exit"
|
||||||
|
|
||||||
const val hideBottomBar = "pref_hide_bottom_bar_on_scroll"
|
const val hideBottomBarOnScroll = "pref_hide_bottom_bar_on_scroll"
|
||||||
|
|
||||||
const val rotation = "pref_rotation_type_key"
|
const val sideNavIconAlignment = "pref_side_nav_icon_alignment"
|
||||||
|
|
||||||
const val enableTransitions = "pref_enable_transitions_key"
|
const val enableTransitions = "pref_enable_transitions_key"
|
||||||
|
|
||||||
@ -23,7 +23,13 @@ object PreferenceKeys {
|
|||||||
|
|
||||||
const val showPageNumber = "pref_show_page_number_key"
|
const val showPageNumber = "pref_show_page_number_key"
|
||||||
|
|
||||||
const val dualPageSplit = "pref_dual_page_split"
|
const val dualPageSplitPaged = "pref_dual_page_split"
|
||||||
|
|
||||||
|
const val dualPageSplitWebtoon = "pref_dual_page_split_webtoon"
|
||||||
|
|
||||||
|
const val dualPageInvertPaged = "pref_dual_page_invert"
|
||||||
|
|
||||||
|
const val dualPageInvertWebtoon = "pref_dual_page_invert_webtoon"
|
||||||
|
|
||||||
const val showReadingMode = "pref_show_reading_mode"
|
const val showReadingMode = "pref_show_reading_mode"
|
||||||
|
|
||||||
@ -45,7 +51,13 @@ object PreferenceKeys {
|
|||||||
|
|
||||||
const val colorFilterMode = "color_filter_mode"
|
const val colorFilterMode = "color_filter_mode"
|
||||||
|
|
||||||
const val defaultViewer = "pref_default_viewer_key"
|
const val grayscale = "pref_grayscale"
|
||||||
|
|
||||||
|
const val invertedColors = "pref_inverted_colors"
|
||||||
|
|
||||||
|
const val defaultReadingMode = "pref_default_reading_mode_key"
|
||||||
|
|
||||||
|
const val defaultOrientationType = "pref_default_orientation_type_key"
|
||||||
|
|
||||||
const val imageScaleType = "pref_image_scale_type_key"
|
const val imageScaleType = "pref_image_scale_type_key"
|
||||||
|
|
||||||
@ -73,6 +85,12 @@ object PreferenceKeys {
|
|||||||
|
|
||||||
const val navigationModeWebtoon = "reader_navigation_mode_webtoon"
|
const val navigationModeWebtoon = "reader_navigation_mode_webtoon"
|
||||||
|
|
||||||
|
const val showNavigationOverlayNewUser = "reader_navigation_overlay_new_user"
|
||||||
|
|
||||||
|
const val showNavigationOverlayOnStart = "reader_navigation_overlay_on_start"
|
||||||
|
|
||||||
|
const val readerHideThreshold = "reader_hide_threshold"
|
||||||
|
|
||||||
const val webtoonSidePadding = "webtoon_side_padding"
|
const val webtoonSidePadding = "webtoon_side_padding"
|
||||||
|
|
||||||
const val portraitColumns = "pref_library_columns_portrait_key"
|
const val portraitColumns = "pref_library_columns_portrait_key"
|
||||||
@ -99,6 +117,8 @@ object PreferenceKeys {
|
|||||||
|
|
||||||
const val downloadOnlyOverWifi = "pref_download_only_over_wifi_key"
|
const val downloadOnlyOverWifi = "pref_download_only_over_wifi_key"
|
||||||
|
|
||||||
|
const val folderPerManga = "create_folder_per_manga"
|
||||||
|
|
||||||
const val numberOfBackups = "backup_slots"
|
const val numberOfBackups = "backup_slots"
|
||||||
|
|
||||||
const val backupInterval = "backup_interval"
|
const val backupInterval = "backup_interval"
|
||||||
@ -114,6 +134,7 @@ object PreferenceKeys {
|
|||||||
const val libraryUpdateRestriction = "library_update_restriction"
|
const val libraryUpdateRestriction = "library_update_restriction"
|
||||||
|
|
||||||
const val libraryUpdateCategories = "library_update_categories"
|
const val libraryUpdateCategories = "library_update_categories"
|
||||||
|
const val libraryUpdateCategoriesExclude = "library_update_categories_exclude"
|
||||||
|
|
||||||
const val libraryUpdatePrioritization = "library_update_prioritization"
|
const val libraryUpdatePrioritization = "library_update_prioritization"
|
||||||
|
|
||||||
@ -128,16 +149,18 @@ object PreferenceKeys {
|
|||||||
const val filterTracked = "pref_filter_library_tracked"
|
const val filterTracked = "pref_filter_library_tracked"
|
||||||
|
|
||||||
const val librarySortingMode = "library_sorting_mode"
|
const val librarySortingMode = "library_sorting_mode"
|
||||||
|
const val librarySortingDirection = "library_sorting_ascending"
|
||||||
|
|
||||||
|
const val migrationSortingMode = "pref_migration_sorting"
|
||||||
|
const val migrationSortingDirection = "pref_migration_direction"
|
||||||
|
|
||||||
const val automaticExtUpdates = "automatic_ext_updates"
|
const val automaticExtUpdates = "automatic_ext_updates"
|
||||||
|
|
||||||
const val showNsfwSource = "show_nsfw_source"
|
const val showNsfwSource = "show_nsfw_source"
|
||||||
const val showNsfwExtension = "show_nsfw_extension"
|
|
||||||
const val labelNsfwExtension = "label_nsfw_extension"
|
|
||||||
|
|
||||||
const val startScreen = "start_screen"
|
const val startScreen = "start_screen"
|
||||||
|
|
||||||
const val useBiometricLock = "use_biometric_lock"
|
const val useAuthenticator = "use_biometric_lock"
|
||||||
|
|
||||||
const val lockAppAfter = "lock_app_after"
|
const val lockAppAfter = "lock_app_after"
|
||||||
|
|
||||||
@ -149,20 +172,23 @@ object PreferenceKeys {
|
|||||||
|
|
||||||
const val autoUpdateMetadata = "auto_update_metadata"
|
const val autoUpdateMetadata = "auto_update_metadata"
|
||||||
|
|
||||||
const val showLibraryUpdateErrors = "show_library_update_errors"
|
const val autoUpdateTrackers = "auto_update_trackers"
|
||||||
|
|
||||||
const val downloadNew = "download_new"
|
const val downloadNew = "download_new"
|
||||||
|
|
||||||
const val downloadNewCategories = "download_new_categories"
|
const val downloadNewCategories = "download_new_categories"
|
||||||
|
const val downloadNewCategoriesExclude = "download_new_categories_exclude"
|
||||||
|
const val removeExcludeCategories = "remove_exclude_categories"
|
||||||
|
|
||||||
const val libraryDisplayMode = "pref_display_mode_library"
|
const val libraryDisplayMode = "pref_display_mode_library"
|
||||||
|
|
||||||
const val lang = "app_language"
|
const val relativeTime: String = "relative_time"
|
||||||
|
|
||||||
const val dateFormat = "app_date_format"
|
const val dateFormat = "app_date_format"
|
||||||
|
|
||||||
const val defaultCategory = "default_category"
|
const val defaultCategory = "default_category"
|
||||||
|
|
||||||
|
const val categorizedDisplay = "categorized_display"
|
||||||
|
|
||||||
const val skipRead = "skip_read"
|
const val skipRead = "skip_read"
|
||||||
|
|
||||||
const val skipFiltered = "skip_filtered"
|
const val skipFiltered = "skip_filtered"
|
||||||
@ -171,6 +197,8 @@ object PreferenceKeys {
|
|||||||
|
|
||||||
const val unreadBadge = "display_unread_badge"
|
const val unreadBadge = "display_unread_badge"
|
||||||
|
|
||||||
|
const val localBadge = "display_local_badge"
|
||||||
|
|
||||||
const val categoryTabs = "display_category_tabs"
|
const val categoryTabs = "display_category_tabs"
|
||||||
|
|
||||||
const val categoryNumberOfItems = "display_number_of_items"
|
const val categoryNumberOfItems = "display_number_of_items"
|
||||||
@ -179,7 +207,7 @@ object PreferenceKeys {
|
|||||||
|
|
||||||
const val searchPinnedSourcesOnly = "search_pinned_sources_only"
|
const val searchPinnedSourcesOnly = "search_pinned_sources_only"
|
||||||
|
|
||||||
const val enableDoh = "enable_doh"
|
const val dohProvider = "doh_provider"
|
||||||
|
|
||||||
const val defaultChapterFilterByRead = "default_chapter_filter_by_read"
|
const val defaultChapterFilterByRead = "default_chapter_filter_by_read"
|
||||||
|
|
||||||
@ -195,7 +223,9 @@ object PreferenceKeys {
|
|||||||
|
|
||||||
const val incognitoMode = "incognito_mode"
|
const val incognitoMode = "incognito_mode"
|
||||||
|
|
||||||
const val createLegacyBackup = "create_legacy_backup"
|
const val tabletUiMode = "tablet_ui_mode"
|
||||||
|
|
||||||
|
const val extensionInstaller = "extension_installer"
|
||||||
|
|
||||||
fun trackUsername(syncId: Int) = "pref_mangasync_username_$syncId"
|
fun trackUsername(syncId: Int) = "pref_mangasync_username_$syncId"
|
||||||
|
|
||||||
|
@ -1,10 +1,17 @@
|
|||||||
package eu.kanade.tachiyomi.data.preference
|
package eu.kanade.tachiyomi.data.preference
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
|
||||||
|
const val UNMETERED_NETWORK = "wifi"
|
||||||
|
const val CHARGING = "ac"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class stores the values for the preferences in the application.
|
* This class stores the values for the preferences in the application.
|
||||||
*/
|
*/
|
||||||
object PreferenceValues {
|
object PreferenceValues {
|
||||||
|
|
||||||
|
/* ktlint-disable experimental:enum-entry-name-case */
|
||||||
|
|
||||||
// Keys are lowercase to match legacy string values
|
// Keys are lowercase to match legacy string values
|
||||||
enum class ThemeMode {
|
enum class ThemeMode {
|
||||||
light,
|
light,
|
||||||
@ -12,29 +19,48 @@ object PreferenceValues {
|
|||||||
system,
|
system,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keys are lowercase to match legacy string values
|
/* ktlint-enable experimental:enum-entry-name-case */
|
||||||
enum class LightThemeVariant {
|
|
||||||
default,
|
|
||||||
blue,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keys are lowercase to match legacy string values
|
enum class AppTheme(val titleResId: Int?) {
|
||||||
enum class DarkThemeVariant {
|
DEFAULT(R.string.label_default),
|
||||||
default,
|
MONET(R.string.theme_monet),
|
||||||
blue,
|
MIDNIGHT_DUSK(R.string.theme_midnightdusk),
|
||||||
amoled,
|
STRAWBERRY_DAIQUIRI(R.string.theme_strawberrydaiquiri),
|
||||||
}
|
YOTSUBA(R.string.theme_yotsuba),
|
||||||
|
TAKO(R.string.theme_tako),
|
||||||
|
GREEN_APPLE(R.string.theme_greenapple),
|
||||||
|
TEALTURQUOISE(R.string.theme_tealturquoise),
|
||||||
|
YINYANG(R.string.theme_yinyang),
|
||||||
|
BLUE(R.string.theme_blue),
|
||||||
|
|
||||||
enum class DisplayMode {
|
// Deprecated
|
||||||
COMPACT_GRID,
|
DARK_BLUE(null),
|
||||||
COMFORTABLE_GRID,
|
HOT_PINK(null),
|
||||||
LIST,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class TappingInvertMode(val shouldInvertHorizontal: Boolean = false, val shouldInvertVertical: Boolean = false) {
|
enum class TappingInvertMode(val shouldInvertHorizontal: Boolean = false, val shouldInvertVertical: Boolean = false) {
|
||||||
NONE,
|
NONE,
|
||||||
HORIZONTAL(shouldInvertHorizontal = true),
|
HORIZONTAL(shouldInvertHorizontal = true),
|
||||||
VERTICAL(shouldInvertVertical = true),
|
VERTICAL(shouldInvertVertical = true),
|
||||||
BOTH(shouldInvertHorizontal = true, shouldInvertVertical = true)
|
BOTH(shouldInvertHorizontal = true, shouldInvertVertical = true),
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class ReaderHideThreshold(val threshold: Int) {
|
||||||
|
HIGHEST(5),
|
||||||
|
HIGH(13),
|
||||||
|
LOW(31),
|
||||||
|
LOWEST(47),
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class TabletUiMode {
|
||||||
|
ALWAYS,
|
||||||
|
LANDSCAPE,
|
||||||
|
NEVER,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class ExtensionInstaller {
|
||||||
|
LEGACY,
|
||||||
|
PACKAGEINSTALLER,
|
||||||
|
SHIZUKU
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,9 +9,17 @@ import com.tfcporciuncula.flow.FlowSharedPreferences
|
|||||||
import com.tfcporciuncula.flow.Preference
|
import com.tfcporciuncula.flow.Preference
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues.DisplayMode
|
import eu.kanade.tachiyomi.data.preference.PreferenceValues.ThemeMode.system
|
||||||
import eu.kanade.tachiyomi.data.track.TrackService
|
import eu.kanade.tachiyomi.data.track.TrackService
|
||||||
import eu.kanade.tachiyomi.data.track.anilist.Anilist
|
import eu.kanade.tachiyomi.data.track.anilist.Anilist
|
||||||
|
import eu.kanade.tachiyomi.ui.browse.migration.sources.MigrationSourcesController
|
||||||
|
import eu.kanade.tachiyomi.ui.library.setting.DisplayModeSetting
|
||||||
|
import eu.kanade.tachiyomi.ui.library.setting.SortDirectionSetting
|
||||||
|
import eu.kanade.tachiyomi.ui.library.setting.SortModeSetting
|
||||||
|
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
|
||||||
|
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
|
||||||
|
import eu.kanade.tachiyomi.util.system.MiuiUtil
|
||||||
|
import eu.kanade.tachiyomi.util.system.isTablet
|
||||||
import eu.kanade.tachiyomi.widget.ExtendedNavigationView
|
import eu.kanade.tachiyomi.widget.ExtendedNavigationView
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
@ -22,7 +30,7 @@ import java.util.Locale
|
|||||||
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
|
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues as Values
|
import eu.kanade.tachiyomi.data.preference.PreferenceValues as Values
|
||||||
|
|
||||||
fun <T> Preference<T>.asImmediateFlow(block: (value: T) -> Unit): Flow<T> {
|
fun <T> Preference<T>.asImmediateFlow(block: (T) -> Unit): Flow<T> {
|
||||||
block(get())
|
block(get())
|
||||||
return asFlow()
|
return asFlow()
|
||||||
.onEach { block(it) }
|
.onEach { block(it) }
|
||||||
@ -36,6 +44,11 @@ operator fun <T> Preference<Set<T>>.minusAssign(item: T) {
|
|||||||
set(get() - item)
|
set(get() - item)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun Preference<Boolean>.toggle(): Boolean {
|
||||||
|
set(!get())
|
||||||
|
return get()
|
||||||
|
}
|
||||||
|
|
||||||
class PreferencesHelper(val context: Context) {
|
class PreferencesHelper(val context: Context) {
|
||||||
|
|
||||||
private val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
private val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
@ -57,9 +70,11 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun confirmExit() = prefs.getBoolean(Keys.confirmExit, false)
|
fun confirmExit() = prefs.getBoolean(Keys.confirmExit, false)
|
||||||
|
|
||||||
fun hideBottomBar() = flowPrefs.getBoolean(Keys.hideBottomBar, true)
|
fun hideBottomBarOnScroll() = flowPrefs.getBoolean(Keys.hideBottomBarOnScroll, true)
|
||||||
|
|
||||||
fun useBiometricLock() = flowPrefs.getBoolean(Keys.useBiometricLock, false)
|
fun sideNavIconAlignment() = flowPrefs.getInt(Keys.sideNavIconAlignment, 0)
|
||||||
|
|
||||||
|
fun useAuthenticator() = flowPrefs.getBoolean(Keys.useAuthenticator, false)
|
||||||
|
|
||||||
fun lockAppAfter() = flowPrefs.getInt(Keys.lockAppAfter, 0)
|
fun lockAppAfter() = flowPrefs.getInt(Keys.lockAppAfter, 0)
|
||||||
|
|
||||||
@ -71,17 +86,13 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun autoUpdateMetadata() = prefs.getBoolean(Keys.autoUpdateMetadata, false)
|
fun autoUpdateMetadata() = prefs.getBoolean(Keys.autoUpdateMetadata, false)
|
||||||
|
|
||||||
fun showLibraryUpdateErrors() = prefs.getBoolean(Keys.showLibraryUpdateErrors, false)
|
fun autoUpdateTrackers() = prefs.getBoolean(Keys.autoUpdateTrackers, false)
|
||||||
|
|
||||||
fun clear() = prefs.edit { clear() }
|
fun themeMode() = flowPrefs.getEnum(Keys.themeMode, system)
|
||||||
|
|
||||||
fun themeMode() = flowPrefs.getEnum(Keys.themeMode, Values.ThemeMode.system)
|
fun appTheme() = flowPrefs.getEnum(Keys.appTheme, Values.AppTheme.DEFAULT)
|
||||||
|
|
||||||
fun themeLight() = flowPrefs.getEnum(Keys.themeLight, Values.LightThemeVariant.default)
|
fun themeDarkAmoled() = flowPrefs.getBoolean(Keys.themeDarkAmoled, false)
|
||||||
|
|
||||||
fun themeDark() = flowPrefs.getEnum(Keys.themeDark, Values.DarkThemeVariant.default)
|
|
||||||
|
|
||||||
fun rotation() = flowPrefs.getInt(Keys.rotation, 1)
|
|
||||||
|
|
||||||
fun pageTransitions() = flowPrefs.getBoolean(Keys.enableTransitions, true)
|
fun pageTransitions() = flowPrefs.getBoolean(Keys.enableTransitions, true)
|
||||||
|
|
||||||
@ -89,7 +100,13 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun showPageNumber() = flowPrefs.getBoolean(Keys.showPageNumber, true)
|
fun showPageNumber() = flowPrefs.getBoolean(Keys.showPageNumber, true)
|
||||||
|
|
||||||
fun dualPageSplit() = flowPrefs.getBoolean(Keys.dualPageSplit, false)
|
fun dualPageSplitPaged() = flowPrefs.getBoolean(Keys.dualPageSplitPaged, false)
|
||||||
|
|
||||||
|
fun dualPageSplitWebtoon() = flowPrefs.getBoolean(Keys.dualPageSplitWebtoon, false)
|
||||||
|
|
||||||
|
fun dualPageInvertPaged() = flowPrefs.getBoolean(Keys.dualPageInvertPaged, false)
|
||||||
|
|
||||||
|
fun dualPageInvertWebtoon() = flowPrefs.getBoolean(Keys.dualPageInvertWebtoon, false)
|
||||||
|
|
||||||
fun showReadingMode() = prefs.getBoolean(Keys.showReadingMode, true)
|
fun showReadingMode() = prefs.getBoolean(Keys.showReadingMode, true)
|
||||||
|
|
||||||
@ -111,7 +128,13 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun colorFilterMode() = flowPrefs.getInt(Keys.colorFilterMode, 0)
|
fun colorFilterMode() = flowPrefs.getInt(Keys.colorFilterMode, 0)
|
||||||
|
|
||||||
fun defaultViewer() = prefs.getInt(Keys.defaultViewer, 2)
|
fun grayscale() = flowPrefs.getBoolean(Keys.grayscale, false)
|
||||||
|
|
||||||
|
fun invertedColors() = flowPrefs.getBoolean(Keys.invertedColors, false)
|
||||||
|
|
||||||
|
fun defaultReadingMode() = prefs.getInt(Keys.defaultReadingMode, ReadingModeType.RIGHT_TO_LEFT.flagValue)
|
||||||
|
|
||||||
|
fun defaultOrientationType() = prefs.getInt(Keys.defaultOrientationType, OrientationType.FREE.flagValue)
|
||||||
|
|
||||||
fun imageScaleType() = flowPrefs.getInt(Keys.imageScaleType, 1)
|
fun imageScaleType() = flowPrefs.getInt(Keys.imageScaleType, 1)
|
||||||
|
|
||||||
@ -143,6 +166,12 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun navigationModeWebtoon() = flowPrefs.getInt(Keys.navigationModeWebtoon, 0)
|
fun navigationModeWebtoon() = flowPrefs.getInt(Keys.navigationModeWebtoon, 0)
|
||||||
|
|
||||||
|
fun showNavigationOverlayNewUser() = flowPrefs.getBoolean(Keys.showNavigationOverlayNewUser, true)
|
||||||
|
|
||||||
|
fun showNavigationOverlayOnStart() = flowPrefs.getBoolean(Keys.showNavigationOverlayOnStart, false)
|
||||||
|
|
||||||
|
fun readerHideTreshold() = flowPrefs.getEnum(Keys.readerHideThreshold, Values.ReaderHideThreshold.LOW)
|
||||||
|
|
||||||
fun portraitColumns() = flowPrefs.getInt(Keys.portraitColumns, 0)
|
fun portraitColumns() = flowPrefs.getInt(Keys.portraitColumns, 0)
|
||||||
|
|
||||||
fun landscapeColumns() = flowPrefs.getInt(Keys.landscapeColumns, 0)
|
fun landscapeColumns() = flowPrefs.getInt(Keys.landscapeColumns, 0)
|
||||||
@ -159,9 +188,9 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun lastVersionCode() = flowPrefs.getInt("last_version_code", 0)
|
fun lastVersionCode() = flowPrefs.getInt("last_version_code", 0)
|
||||||
|
|
||||||
fun sourceDisplayMode() = flowPrefs.getEnum(Keys.sourceDisplayMode, DisplayMode.COMPACT_GRID)
|
fun sourceDisplayMode() = flowPrefs.getEnum(Keys.sourceDisplayMode, DisplayModeSetting.COMPACT_GRID)
|
||||||
|
|
||||||
fun enabledLanguages() = flowPrefs.getStringSet(Keys.enabledLanguages, setOf("en", Locale.getDefault().language))
|
fun enabledLanguages() = flowPrefs.getStringSet(Keys.enabledLanguages, setOf("all", "en", Locale.getDefault().language))
|
||||||
|
|
||||||
fun trackUsername(sync: TrackService) = prefs.getString(Keys.trackUsername(sync.id), "")
|
fun trackUsername(sync: TrackService) = prefs.getString(Keys.trackUsername(sync.id), "")
|
||||||
|
|
||||||
@ -180,6 +209,8 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun backupsDirectory() = flowPrefs.getString(Keys.backupDirectory, defaultBackupDir.toString())
|
fun backupsDirectory() = flowPrefs.getString(Keys.backupDirectory, defaultBackupDir.toString())
|
||||||
|
|
||||||
|
fun relativeTime() = flowPrefs.getInt(Keys.relativeTime, 7)
|
||||||
|
|
||||||
fun dateFormat(format: String = flowPrefs.getString(Keys.dateFormat, "").get()): DateFormat = when (format) {
|
fun dateFormat(format: String = flowPrefs.getString(Keys.dateFormat, "").get()): DateFormat = when (format) {
|
||||||
"" -> DateFormat.getDateInstance(DateFormat.SHORT)
|
"" -> DateFormat.getDateInstance(DateFormat.SHORT)
|
||||||
else -> SimpleDateFormat(format, Locale.getDefault())
|
else -> SimpleDateFormat(format, Locale.getDefault())
|
||||||
@ -189,6 +220,8 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun downloadOnlyOverWifi() = prefs.getBoolean(Keys.downloadOnlyOverWifi, true)
|
fun downloadOnlyOverWifi() = prefs.getBoolean(Keys.downloadOnlyOverWifi, true)
|
||||||
|
|
||||||
|
fun folderPerManga() = prefs.getBoolean(Keys.folderPerManga, false)
|
||||||
|
|
||||||
fun numberOfBackups() = flowPrefs.getInt(Keys.numberOfBackups, 1)
|
fun numberOfBackups() = flowPrefs.getInt(Keys.numberOfBackups, 1)
|
||||||
|
|
||||||
fun backupInterval() = flowPrefs.getInt(Keys.backupInterval, 0)
|
fun backupInterval() = flowPrefs.getInt(Keys.backupInterval, 0)
|
||||||
@ -199,18 +232,23 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun removeBookmarkedChapters() = prefs.getBoolean(Keys.removeBookmarkedChapters, false)
|
fun removeBookmarkedChapters() = prefs.getBoolean(Keys.removeBookmarkedChapters, false)
|
||||||
|
|
||||||
|
fun removeExcludeCategories() = flowPrefs.getStringSet(Keys.removeExcludeCategories, emptySet())
|
||||||
|
|
||||||
fun libraryUpdateInterval() = flowPrefs.getInt(Keys.libraryUpdateInterval, 24)
|
fun libraryUpdateInterval() = flowPrefs.getInt(Keys.libraryUpdateInterval, 24)
|
||||||
|
|
||||||
fun libraryUpdateRestriction() = prefs.getStringSet(Keys.libraryUpdateRestriction, setOf("wifi"))
|
fun libraryUpdateRestriction() = flowPrefs.getStringSet(Keys.libraryUpdateRestriction, setOf(UNMETERED_NETWORK))
|
||||||
|
|
||||||
fun libraryUpdateCategories() = flowPrefs.getStringSet(Keys.libraryUpdateCategories, emptySet())
|
fun libraryUpdateCategories() = flowPrefs.getStringSet(Keys.libraryUpdateCategories, emptySet())
|
||||||
|
fun libraryUpdateCategoriesExclude() = flowPrefs.getStringSet(Keys.libraryUpdateCategoriesExclude, emptySet())
|
||||||
|
|
||||||
fun libraryUpdatePrioritization() = flowPrefs.getInt(Keys.libraryUpdatePrioritization, 0)
|
fun libraryUpdatePrioritization() = flowPrefs.getInt(Keys.libraryUpdatePrioritization, 0)
|
||||||
|
|
||||||
fun libraryDisplayMode() = flowPrefs.getEnum(Keys.libraryDisplayMode, DisplayMode.COMPACT_GRID)
|
fun libraryDisplayMode() = flowPrefs.getEnum(Keys.libraryDisplayMode, DisplayModeSetting.COMPACT_GRID)
|
||||||
|
|
||||||
fun downloadBadge() = flowPrefs.getBoolean(Keys.downloadBadge, false)
|
fun downloadBadge() = flowPrefs.getBoolean(Keys.downloadBadge, false)
|
||||||
|
|
||||||
|
fun localBadge() = flowPrefs.getBoolean(Keys.localBadge, true)
|
||||||
|
|
||||||
fun downloadedOnly() = flowPrefs.getBoolean(Keys.downloadedOnly, false)
|
fun downloadedOnly() = flowPrefs.getBoolean(Keys.downloadedOnly, false)
|
||||||
|
|
||||||
fun unreadBadge() = flowPrefs.getBoolean(Keys.unreadBadge, true)
|
fun unreadBadge() = flowPrefs.getBoolean(Keys.unreadBadge, true)
|
||||||
@ -227,18 +265,19 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun filterTracking(name: Int) = flowPrefs.getInt("${Keys.filterTracked}_$name", ExtendedNavigationView.Item.TriStateGroup.State.IGNORE.value)
|
fun filterTracking(name: Int) = flowPrefs.getInt("${Keys.filterTracked}_$name", ExtendedNavigationView.Item.TriStateGroup.State.IGNORE.value)
|
||||||
|
|
||||||
fun librarySortingMode() = flowPrefs.getInt(Keys.librarySortingMode, 0)
|
fun librarySortingMode() = flowPrefs.getEnum(Keys.librarySortingMode, SortModeSetting.ALPHABETICAL)
|
||||||
|
fun librarySortingAscending() = flowPrefs.getEnum(Keys.librarySortingDirection, SortDirectionSetting.ASCENDING)
|
||||||
|
|
||||||
fun librarySortingAscending() = flowPrefs.getBoolean("library_sorting_ascending", true)
|
fun migrationSortingMode() = flowPrefs.getEnum(Keys.migrationSortingMode, MigrationSourcesController.SortSetting.ALPHABETICAL)
|
||||||
|
fun migrationSortingDirection() = flowPrefs.getEnum(Keys.migrationSortingDirection, MigrationSourcesController.DirectionSetting.ASCENDING)
|
||||||
|
|
||||||
fun automaticExtUpdates() = flowPrefs.getBoolean(Keys.automaticExtUpdates, true)
|
fun automaticExtUpdates() = flowPrefs.getBoolean(Keys.automaticExtUpdates, true)
|
||||||
|
|
||||||
fun showNsfwSource() = flowPrefs.getBoolean(Keys.showNsfwSource, true)
|
fun showNsfwSource() = flowPrefs.getBoolean(Keys.showNsfwSource, true)
|
||||||
fun showNsfwExtension() = flowPrefs.getBoolean(Keys.showNsfwExtension, true)
|
|
||||||
fun labelNsfwExtension() = prefs.getBoolean(Keys.labelNsfwExtension, true)
|
|
||||||
|
|
||||||
fun extensionUpdatesCount() = flowPrefs.getInt("ext_updates_count", 0)
|
fun extensionUpdatesCount() = flowPrefs.getInt("ext_updates_count", 0)
|
||||||
|
|
||||||
|
fun lastAppCheck() = flowPrefs.getLong("last_app_check", 0)
|
||||||
fun lastExtCheck() = flowPrefs.getLong("last_ext_check", 0)
|
fun lastExtCheck() = flowPrefs.getLong("last_ext_check", 0)
|
||||||
|
|
||||||
fun searchPinnedSourcesOnly() = prefs.getBoolean(Keys.searchPinnedSourcesOnly, false)
|
fun searchPinnedSourcesOnly() = prefs.getBoolean(Keys.searchPinnedSourcesOnly, false)
|
||||||
@ -250,11 +289,12 @@ class PreferencesHelper(val context: Context) {
|
|||||||
fun downloadNew() = flowPrefs.getBoolean(Keys.downloadNew, false)
|
fun downloadNew() = flowPrefs.getBoolean(Keys.downloadNew, false)
|
||||||
|
|
||||||
fun downloadNewCategories() = flowPrefs.getStringSet(Keys.downloadNewCategories, emptySet())
|
fun downloadNewCategories() = flowPrefs.getStringSet(Keys.downloadNewCategories, emptySet())
|
||||||
|
fun downloadNewCategoriesExclude() = flowPrefs.getStringSet(Keys.downloadNewCategoriesExclude, emptySet())
|
||||||
fun lang() = prefs.getString(Keys.lang, "")
|
|
||||||
|
|
||||||
fun defaultCategory() = prefs.getInt(Keys.defaultCategory, -1)
|
fun defaultCategory() = prefs.getInt(Keys.defaultCategory, -1)
|
||||||
|
|
||||||
|
fun categorisedDisplaySettings() = flowPrefs.getBoolean(Keys.categorizedDisplay, false)
|
||||||
|
|
||||||
fun skipRead() = prefs.getBoolean(Keys.skipRead, false)
|
fun skipRead() = prefs.getBoolean(Keys.skipRead, false)
|
||||||
|
|
||||||
fun skipFiltered() = prefs.getBoolean(Keys.skipFiltered, true)
|
fun skipFiltered() = prefs.getBoolean(Keys.skipFiltered, true)
|
||||||
@ -263,7 +303,7 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun trustedSignatures() = flowPrefs.getStringSet("trusted_signatures", emptySet())
|
fun trustedSignatures() = flowPrefs.getStringSet("trusted_signatures", emptySet())
|
||||||
|
|
||||||
fun enableDoh() = prefs.getBoolean(Keys.enableDoh, false)
|
fun dohProvider() = prefs.getInt(Keys.dohProvider, -1)
|
||||||
|
|
||||||
fun lastSearchQuerySearchSettings() = flowPrefs.getString("last_search_query", "")
|
fun lastSearchQuerySearchSettings() = flowPrefs.getString("last_search_query", "")
|
||||||
|
|
||||||
@ -273,15 +313,23 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun filterChapterByBookmarked() = prefs.getInt(Keys.defaultChapterFilterByBookmarked, Manga.SHOW_ALL)
|
fun filterChapterByBookmarked() = prefs.getInt(Keys.defaultChapterFilterByBookmarked, Manga.SHOW_ALL)
|
||||||
|
|
||||||
fun sortChapterBySourceOrNumber() = prefs.getInt(Keys.defaultChapterSortBySourceOrNumber, Manga.SORTING_SOURCE)
|
fun sortChapterBySourceOrNumber() = prefs.getInt(Keys.defaultChapterSortBySourceOrNumber, Manga.CHAPTER_SORTING_SOURCE)
|
||||||
|
|
||||||
fun displayChapterByNameOrNumber() = prefs.getInt(Keys.defaultChapterDisplayByNameOrNumber, Manga.DISPLAY_NAME)
|
fun displayChapterByNameOrNumber() = prefs.getInt(Keys.defaultChapterDisplayByNameOrNumber, Manga.CHAPTER_DISPLAY_NAME)
|
||||||
|
|
||||||
fun sortChapterByAscendingOrDescending() = prefs.getInt(Keys.defaultChapterSortByAscendingOrDescending, Manga.SORT_DESC)
|
fun sortChapterByAscendingOrDescending() = prefs.getInt(Keys.defaultChapterSortByAscendingOrDescending, Manga.CHAPTER_SORT_DESC)
|
||||||
|
|
||||||
fun incognitoMode() = flowPrefs.getBoolean(Keys.incognitoMode, false)
|
fun incognitoMode() = flowPrefs.getBoolean(Keys.incognitoMode, false)
|
||||||
|
|
||||||
fun createLegacyBackup() = flowPrefs.getBoolean(Keys.createLegacyBackup, true)
|
fun tabletUiMode() = flowPrefs.getEnum(
|
||||||
|
Keys.tabletUiMode,
|
||||||
|
if (context.applicationContext.isTablet()) Values.TabletUiMode.ALWAYS else Values.TabletUiMode.NEVER
|
||||||
|
)
|
||||||
|
|
||||||
|
fun extensionInstaller() = flowPrefs.getEnum(
|
||||||
|
Keys.extensionInstaller,
|
||||||
|
if (MiuiUtil.isMiui()) Values.ExtensionInstaller.LEGACY else Values.ExtensionInstaller.PACKAGEINSTALLER
|
||||||
|
)
|
||||||
|
|
||||||
fun setChapterSettingsDefault(manga: Manga) {
|
fun setChapterSettingsDefault(manga: Manga) {
|
||||||
prefs.edit {
|
prefs.edit {
|
||||||
@ -290,7 +338,7 @@ class PreferencesHelper(val context: Context) {
|
|||||||
putInt(Keys.defaultChapterFilterByBookmarked, manga.bookmarkedFilter)
|
putInt(Keys.defaultChapterFilterByBookmarked, manga.bookmarkedFilter)
|
||||||
putInt(Keys.defaultChapterSortBySourceOrNumber, manga.sorting)
|
putInt(Keys.defaultChapterSortBySourceOrNumber, manga.sorting)
|
||||||
putInt(Keys.defaultChapterDisplayByNameOrNumber, manga.displayMode)
|
putInt(Keys.defaultChapterDisplayByNameOrNumber, manga.displayMode)
|
||||||
putInt(Keys.defaultChapterSortByAscendingOrDescending, if (manga.sortDescending()) Manga.SORT_DESC else Manga.SORT_ASC)
|
putInt(Keys.defaultChapterSortByAscendingOrDescending, if (manga.sortDescending()) Manga.CHAPTER_SORT_DESC else Manga.CHAPTER_SORT_ASC)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,28 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.track
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
|
import eu.kanade.tachiyomi.source.Source
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An Enhanced Track Service will never prompt the user to match a manga with the remote.
|
||||||
|
* It is expected that such Track Service can only work with specific sources and unique IDs.
|
||||||
|
*/
|
||||||
|
interface EnhancedTrackService {
|
||||||
|
/**
|
||||||
|
* This TrackService will only work with the sources that are accepted by this filter function.
|
||||||
|
*/
|
||||||
|
fun accept(source: Source): Boolean {
|
||||||
|
return source::class.qualifiedName in getAcceptedSources()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fully qualified source classes that this track service is compatible with.
|
||||||
|
*/
|
||||||
|
fun getAcceptedSources(): List<String>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* match is similar to TrackService.search, but only return zero or one match.
|
||||||
|
*/
|
||||||
|
suspend fun match(manga: Manga): TrackSearch?
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user