mirror of
https://github.com/mihonapp/mihon.git
synced 2025-08-03 05:11:31 +02:00
Compare commits
1531 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
3222247969 | ||
|
dd6c9ce2fe | ||
|
7446b28ff1 | ||
|
38c6702b8f | ||
|
afcf4b2988 | ||
|
ebb96a6ff4 | ||
|
8b0affe9bd | ||
|
1a25cea0d6 | ||
|
2ecbcdf4bd | ||
|
642b392d44 | ||
|
8dce7b3e9e | ||
|
33e90d6449 | ||
|
50b17d5d34 | ||
|
7818885406 | ||
|
26af7ccc77 | ||
|
5d1f79012e | ||
|
cac80daa71 | ||
|
fc184f1cfa | ||
|
725fcbba0e | ||
|
bdeb209d43 | ||
|
a078f1ab1b | ||
|
86c3d8c064 | ||
|
156191af44 | ||
|
57bba9e5ab | ||
|
dd1923fe88 | ||
|
df773ee15c | ||
|
f5451a6881 | ||
|
fcec1581b7 | ||
|
11cc789e36 | ||
|
16f9fb2f40 | ||
|
6bfaa85e84 | ||
|
8f43fb9530 | ||
|
04d2a3399b | ||
|
054bf8ec5d | ||
|
8417f5a63c | ||
|
26b46cace0 | ||
|
0849111247 | ||
|
f9c25b350e | ||
|
5b12c144da | ||
|
f38130d086 | ||
|
4b60138d41 | ||
|
fde7bfa3d1 | ||
|
69635ee66a | ||
|
224f29077d | ||
|
e1ab1fdb65 | ||
|
3e86cb094b | ||
|
9fbd3fe33f | ||
|
073e9f94ff | ||
|
64c0d9506d | ||
|
f638092ab9 | ||
|
d0c4463ab3 | ||
|
ad107860b9 | ||
|
5efb31bd71 | ||
|
e4a2f35907 | ||
|
e49781de7a | ||
|
37cb4ec0c2 | ||
|
401134fa8e | ||
|
87391832ba | ||
|
e36d31bf0f | ||
|
37b7efbc87 | ||
|
6e4a30e593 | ||
|
ebddb96373 | ||
|
0288abb66e | ||
|
d869a13ef9 | ||
|
ccdfc37c97 | ||
|
37c55abc2a | ||
|
c50b1a5c66 | ||
|
187e9f94aa | ||
|
1704dc062d | ||
|
0657a52924 | ||
|
ccc4144f3c | ||
|
d5b4bb49b1 | ||
|
5b3f9e082e | ||
|
ca06516900 | ||
|
3fb42b6ce9 | ||
|
2cbe946e7e | ||
|
3b5b9a1ae5 | ||
|
a834ff3a44 | ||
|
82b552ac9a | ||
|
15f7e53e4f | ||
|
9792a6cb78 | ||
|
f30150c0f0 | ||
|
5c868d7846 | ||
|
39e41510d0 | ||
|
78b76a186c | ||
|
6e04822f5e | ||
|
4ff5c1148e | ||
|
bd285920cd | ||
|
fb04401460 | ||
|
42bf91779d | ||
|
2ab744c525 | ||
|
4a244a598b | ||
|
d0bff298b7 | ||
|
152eb5b951 | ||
|
d558f9e1d6 | ||
|
b3557e844c | ||
|
9c8ccb8e0e | ||
|
4138a17e29 | ||
|
fbda243c0d | ||
|
eb742b29f8 | ||
|
d2e62ffb19 | ||
|
2921be620a | ||
|
c61a51438d | ||
|
7e40680af0 | ||
|
93925a7286 | ||
|
b04807e53a | ||
|
01e13e59e5 | ||
|
2cf1009f70 | ||
|
93827aba34 | ||
|
3318314c4a | ||
|
44cabf2f0b | ||
|
a8ca7b690f | ||
|
824d5e22bc | ||
|
7a360779b3 | ||
|
4b5f965cea | ||
|
d03cbbe0cd | ||
|
84bcd8d1d2 | ||
|
6756bfab75 | ||
|
8d97b980e3 | ||
|
2d19729869 | ||
|
f5bde3726a | ||
|
ea092fa175 | ||
|
9c4051a5ba | ||
|
fed914827a | ||
|
ea33f8dba5 | ||
|
4f91d80765 | ||
|
4178f945c9 | ||
|
558aad1a71 | ||
|
d6cbff2837 | ||
|
aea0cadbfb | ||
|
e4292719d3 | ||
|
69cdba71eb | ||
|
5c5468f9af | ||
|
6635dd2990 | ||
|
27e5256305 | ||
|
b6dbf63633 | ||
|
551e6a8b62 | ||
|
570fec6ea6 | ||
|
7da32750b2 | ||
|
a2b21e5ad6 | ||
|
dbd93cf5d1 | ||
|
c2eaf1c86b | ||
|
890f1a3c7b | ||
|
3fdcd636d7 | ||
|
3d7e44726d | ||
|
147455f99c | ||
|
b25ca7617d | ||
|
bc1fbfac9d | ||
|
7e92921f84 | ||
|
e1adb89ff8 | ||
|
4e544005fe | ||
|
31bc2c4420 | ||
|
02b3718aa1 | ||
|
26a42ba9c0 | ||
|
b1e104319f | ||
|
a3afb35539 | ||
|
fba244423f | ||
|
8500add09f | ||
|
23bfa1f18f | ||
|
b4f2da12ea | ||
|
b84a31ba92 | ||
|
d0950cb026 | ||
|
404f53b16b | ||
|
737d0fb8f3 | ||
|
b95a30e424 | ||
|
0d9c1e6e9c | ||
|
3bfbd58402 | ||
|
bd9a08c73d | ||
|
41dc41f285 | ||
|
50f959e5f4 | ||
|
4b4be58d0d | ||
|
4bba7a8bab | ||
|
60bcebe4d1 | ||
|
cf6407c4d4 | ||
|
5f8252447f | ||
|
dcd5541e96 | ||
|
7be6863910 | ||
|
caf9219d99 | ||
|
3b62396442 | ||
|
bbe1608006 | ||
|
b8fa326c21 | ||
|
1cf1b34e7f | ||
|
ff4fb83bff | ||
|
e24501da09 | ||
|
0ca14c61c2 | ||
|
6be9cccc7a | ||
|
a5a70defc8 | ||
|
db3cbac310 | ||
|
de23226591 | ||
|
ea8383978b | ||
|
b04d1e5f50 | ||
|
98c459a6b6 | ||
|
00f442b77e | ||
|
42b0e3e438 | ||
|
8d1f99a480 | ||
|
bef8342aa5 | ||
|
2131294b22 | ||
|
5c22cbf28e | ||
|
488276d498 | ||
|
6ac17363ed | ||
|
58c47c4c50 | ||
|
80b2ebc45b | ||
|
ef2c9460b5 | ||
|
ad84a8c3e9 | ||
|
8b9a06e298 | ||
|
6b1d597d34 | ||
|
5a37f2398a | ||
|
98a4f6cccb | ||
|
633bd6eb46 | ||
|
f19c288bec | ||
|
e2ce3f68bf | ||
|
56722140c9 | ||
|
e90b39b29d | ||
|
f4c684b4b8 | ||
|
869396b1a4 | ||
|
7f9222f7b7 | ||
|
a35f947892 | ||
|
ec272f6c4e | ||
|
f0af3858e8 | ||
|
db91d04e82 | ||
|
9859b38f32 | ||
|
0190c36d20 | ||
|
ba533f30ce | ||
|
29fa93e829 | ||
|
0fabe4bd01 | ||
|
f98b4f4e39 | ||
|
b8c1257645 | ||
|
467ceacb17 | ||
|
2d22baba62 | ||
|
750f90614d | ||
|
d28ded4525 | ||
|
4b4a138eee | ||
|
b5dca2eb09 | ||
|
747cbd24cb | ||
|
d3520419d4 | ||
|
acb8ab15b2 | ||
|
5cdcc1679f | ||
|
b37b3767f3 | ||
|
2d56ad1ad9 | ||
|
5d3bc7245e | ||
|
e82963c9ef | ||
|
ec34977a64 | ||
|
2ced56e490 | ||
|
e568951396 | ||
|
e275897bf9 | ||
|
2b089648a3 | ||
|
c2a831dded | ||
|
c740558327 | ||
|
0e3176a77c | ||
|
f85cbb1582 | ||
|
20bbda78e6 | ||
|
0225711f6f | ||
|
7ec822503a | ||
|
83871fc013 | ||
|
b668364afb | ||
|
877ae041a4 | ||
|
1395343f11 | ||
|
30b3b2d3ff | ||
|
f3cecd3cde | ||
|
0086743a53 | ||
|
bc8c45832e | ||
|
4a3070265a | ||
|
f54adb49a1 | ||
|
ec30026333 | ||
|
4ea512f6c2 | ||
|
829aadd0bd | ||
|
9d28def387 | ||
|
86fe850794 | ||
|
30ac94181b | ||
|
48d3d454c0 | ||
|
6865c21c75 | ||
|
82cd316493 | ||
|
7270c48f26 | ||
|
9e5d79aec3 | ||
|
c51e83c048 | ||
|
52fa28c16a | ||
|
935c8e7d82 | ||
|
19be0d68b6 | ||
|
f9bbbce466 | ||
|
eb5ef72747 | ||
|
0215b66098 | ||
|
3dea10bcb9 | ||
|
cd3cb72b65 | ||
|
5b474e96b7 | ||
|
9ce1d71a45 | ||
|
b8cdf7fbff | ||
|
28594bba2c | ||
|
d5c207d8a3 | ||
|
56826fb477 | ||
|
171d7f2b8c | ||
|
5ec5829e77 | ||
|
448978ac8a | ||
|
fb9791f597 | ||
|
07d1b9f3ba | ||
|
6b91f65457 | ||
|
0c7b1bda7f | ||
|
032b377de7 | ||
|
26d8e47bb9 | ||
|
970ff7841e | ||
|
3f62837260 | ||
|
d55c854ebf | ||
|
6b2b21edfa | ||
|
99270e370e | ||
|
c7d09d098a | ||
|
21804bfc45 | ||
|
38950f7bc8 | ||
|
bbf5c86b46 | ||
|
3fa68ed217 | ||
|
cc6aef693e | ||
|
5a320d87e8 | ||
|
da95ecb686 | ||
|
774a87a42a | ||
|
ff4a217730 | ||
|
a43754e1a6 | ||
|
8ef200861c | ||
|
ddd180e56a | ||
|
30b86e530b | ||
|
2f26982e34 | ||
|
504844a892 | ||
|
4c1da1bd1d | ||
|
dc62d0ea8b | ||
|
fddca15182 | ||
|
81f49f34ef | ||
|
c39a1b7867 | ||
|
d4b764fa31 | ||
|
bb54a81ef0 | ||
|
d85af2fec6 | ||
|
90c08303fa | ||
|
92e83f702c | ||
|
084e6a964e | ||
|
532f662b05 | ||
|
53f5ea7fe9 | ||
|
fc6946ed61 | ||
|
f5c7aa1142 | ||
|
761635b572 | ||
|
488d8ab8cf | ||
|
8efb20439a | ||
|
43c195e14a | ||
|
8a3a7418d0 | ||
|
32190b6cac | ||
|
880407442c | ||
|
3b34a878a7 | ||
|
b79340989f | ||
|
0e526c36be | ||
|
a83d29f058 | ||
|
be7108a2ee | ||
|
8e9b1124cd | ||
|
1948d55d5d | ||
|
9c49a5ed22 | ||
|
0bb20a92af | ||
|
cd82c88b9a | ||
|
8d40e20b7d | ||
|
31b62b2779 | ||
|
88b56121a3 | ||
|
d6c0a5ef8b | ||
|
5732fc61e8 | ||
|
655fa25b51 | ||
|
aab5f083db | ||
|
03b9950fa1 | ||
|
2453d1a886 | ||
|
4b9a6541d1 | ||
|
a70b848646 | ||
|
ce44c0615b | ||
|
f207e87722 | ||
|
2e81e1b7d8 | ||
|
605c3de150 | ||
|
7aa073ddca | ||
|
4b0f549666 | ||
|
40749dc767 | ||
|
3599d53c61 | ||
|
2156844b87 | ||
|
763288ab13 | ||
|
58e6479438 | ||
|
6d6c38ecaf | ||
|
3760b310df | ||
|
47b56644de | ||
|
301cae13f0 | ||
|
1fe9b7bda7 | ||
|
324ae3fcfb | ||
|
e36e9d9d5c | ||
|
4228bbb88e | ||
|
09abfc7843 | ||
|
1f34f5277c | ||
|
80b4b7bee6 | ||
|
1f9f9662bc | ||
|
97656935a2 | ||
|
2d690a09b3 | ||
|
29348677b8 | ||
|
1f79444a53 | ||
|
daaa23e8e0 | ||
|
1d6aa9a277 | ||
|
7497e02979 | ||
|
f34dc3be90 | ||
|
65261356eb | ||
|
4291cc8eb1 | ||
|
8811d951d0 | ||
|
9dbc1aa7a3 | ||
|
b0520df1dd | ||
|
a89651810d | ||
|
431c04e54f | ||
|
f461c71625 | ||
|
b635789740 | ||
|
f00e03e5ea | ||
|
6db2becd30 | ||
|
5f378e28b6 | ||
|
4ebceac07f | ||
|
aab5a56892 | ||
|
e58945a209 | ||
|
03e4eb1061 | ||
|
09a3509d79 | ||
|
b3a11eca0f | ||
|
650c2dc6e7 | ||
|
d4adb664cc | ||
|
5194bdb229 | ||
|
87ec71142b | ||
|
85f2996ae9 | ||
|
e296d56e09 | ||
|
dd676b6d14 | ||
|
7c7bd72c8e | ||
|
c7e44aa22f | ||
|
ac4f98e152 | ||
|
e0d23cd688 | ||
|
3966a917ee | ||
|
be33a57d43 | ||
|
4a71022a60 | ||
|
83129385e2 | ||
|
34ac39e7e5 | ||
|
1474c8ffb3 | ||
|
441e7bf8b1 | ||
|
71fc5d6d35 | ||
|
ff996d282a | ||
|
11f640cfee | ||
|
1cbe225a94 | ||
|
d6f1534ee8 | ||
|
24e64f52e2 | ||
|
b0da0753d9 | ||
|
e511f24979 | ||
|
22e83f408b | ||
|
ec96a81735 | ||
|
7892cc1519 | ||
|
f7b11f2ce9 | ||
|
b4e15263db | ||
|
96c3116af6 | ||
|
7845f9430e | ||
|
16abfeeff0 | ||
|
3bc6b1e202 | ||
|
3c2e237d63 | ||
|
7701672d7a | ||
|
2993e3f0f2 | ||
|
688cc64dff | ||
|
9f0052eceb | ||
|
19eb4aaac9 | ||
|
a2bb81b7db | ||
|
5e68fe4fe9 | ||
|
914831d51f | ||
|
5315467908 | ||
|
807987f0d3 | ||
|
b3426f37e7 | ||
|
afceac15c8 | ||
|
3d4e56948d | ||
|
737cf9898d | ||
|
322f3a07e8 | ||
|
6c7b3d7811 | ||
|
bfd22f8f2d | ||
|
2ca62c4eda | ||
|
a2d53c439e | ||
|
29e1976b90 | ||
|
4efb736e56 | ||
|
58acf0a8aa | ||
|
9f5f101858 | ||
|
2a875fe9b8 | ||
|
bb5a5ea25f | ||
|
039fe4a618 | ||
|
0c9c4c0347 | ||
|
819577a15d | ||
|
b563e85c3b | ||
|
99ac30e59f | ||
|
4774deb1ef | ||
|
d49ec41f3a | ||
|
f90e1b935c | ||
|
db93d1da76 | ||
|
7d74b174e0 | ||
|
e513487caa | ||
|
483b204fb5 | ||
|
56028aff55 | ||
|
7336714306 | ||
|
8bde35298f | ||
|
3fe5e53b25 | ||
|
dcafdac036 | ||
|
f8d8cf9f6a | ||
|
5bb1133f0f | ||
|
2b96709799 | ||
|
1c8da5fa97 | ||
|
73901f50c0 | ||
|
76057b84b2 | ||
|
164de67a56 | ||
|
aeffb5eeb8 | ||
|
6f94777530 | ||
|
2e15be59af | ||
|
bc1f6ba517 | ||
|
59f8c1a288 | ||
|
cd9487f94c | ||
|
978489fade | ||
|
07c9af4901 | ||
|
d6977e5676 | ||
|
a843054388 | ||
|
098a7d1deb | ||
|
9ef0af0069 | ||
|
c751851941 | ||
|
9f2ddaadde | ||
|
fc328e141c | ||
|
0e19c245e9 | ||
|
27bac4fffb | ||
|
4bf4b167a5 | ||
|
2b8d1bcc02 | ||
|
e8b7743826 | ||
|
8ea05e852e | ||
|
3547d0142f | ||
|
4d9d587366 | ||
|
e2510c144a | ||
|
00519e3b93 | ||
|
473dc688f0 | ||
|
b635f02d93 | ||
|
d8fb6b893f | ||
|
bdc5d557d1 | ||
|
cbfe9c30bb | ||
|
459b369feb | ||
|
3192d47837 | ||
|
f6f5b6aeab | ||
|
1b2c12385f | ||
|
80c7a45328 | ||
|
2096df301d | ||
|
0b78028cf6 | ||
|
46ac9fe970 | ||
|
b034f503f8 | ||
|
9ebeff04e6 | ||
|
fa73e2403b | ||
|
35ec593658 | ||
|
905c96922b | ||
|
018ca71336 | ||
|
383f7089c4 | ||
|
a21aa8125e | ||
|
83e193f1ab | ||
|
bdbe1c4d0f | ||
|
e5eadb0261 | ||
|
4ee1d72b6f | ||
|
902bb35ba7 | ||
|
4684797dfb | ||
|
386b8945c8 | ||
|
86a018ebad | ||
|
ba93060e59 | ||
|
788583e66f | ||
|
cbcab5a545 | ||
|
634ee86bbd | ||
|
64f60c36e6 | ||
|
0b4f3f5532 | ||
|
d977b89af1 | ||
|
487ce37d91 | ||
|
1551891c15 | ||
|
b15073fd61 | ||
|
e56f6c1017 | ||
|
34906a7425 | ||
|
86bacbe586 | ||
|
14a08f0668 | ||
|
9385b86ecb | ||
|
da7a64b40d | ||
|
ab1a44e108 | ||
|
26ddc6e3aa | ||
|
1dc4a52f61 | ||
|
473a4fec70 | ||
|
1919c2d925 | ||
|
71e31e6c03 | ||
|
c01df7f0a1 | ||
|
6024f6175b | ||
|
33500e5b69 | ||
|
17899a6d6d | ||
|
4c3eb68d3a | ||
|
29ced9642d | ||
|
af82591d85 | ||
|
5bc4a446ec | ||
|
83e93b254e | ||
|
49c7dd0cac | ||
|
96d2fb62e4 | ||
|
c76a136d3f | ||
|
940409a4c3 | ||
|
071dd88ef8 | ||
|
a58a4634e2 | ||
|
5979e72662 | ||
|
010436e797 | ||
|
980709cccb | ||
|
fe80356756 | ||
|
cecf532ffd | ||
|
6cb255e60a | ||
|
b46fb7d1e1 | ||
|
8874193927 | ||
|
a4515ad251 | ||
|
55b0b57699 | ||
|
aab7795b4c | ||
|
196a8e6829 | ||
|
972cd98d7b | ||
|
a16b5d241b | ||
|
bfa918140f | ||
|
0721de5b81 | ||
|
a409fde519 | ||
|
8e34a30dce | ||
|
ba43462041 | ||
|
c8ae936ce9 | ||
|
853f949140 | ||
|
615b01a006 | ||
|
0eb5a3176b | ||
|
867a5a3ea0 | ||
|
5159eabc5d | ||
|
9357af2bcf | ||
|
038532897b | ||
|
325a5e37aa | ||
|
7d3fe0ed43 | ||
|
eef95cef33 | ||
|
591df8abcc | ||
|
46734c525f | ||
|
a3378e6080 | ||
|
3791d82540 | ||
|
a3ab8746bf | ||
|
069bd90c0f | ||
|
68697e59d7 | ||
|
b3dd8b7355 | ||
|
eb2a904b61 | ||
|
17951cfd68 | ||
|
6d6237e370 | ||
|
851a5ab7e4 | ||
|
d3ce46a367 | ||
|
74c5b29484 | ||
|
20453dc08f | ||
|
9e3b454b1b | ||
|
29633b64aa | ||
|
76c0ead1db | ||
|
2674570792 | ||
|
21771e62aa | ||
|
5d77ee37d2 | ||
|
2dfbfd0958 | ||
|
05085fe57f | ||
|
ff32ab09fb | ||
|
deaded5af2 | ||
|
f3c50ee9a3 | ||
|
3072296919 | ||
|
1f10b79ee8 | ||
|
b9e108eb4d | ||
|
f26cfa58e4 | ||
|
e1525a5125 | ||
|
388dc2f103 | ||
|
7e4c45858f | ||
|
d476431707 | ||
|
284445c364 | ||
|
08d1ecfba7 | ||
|
0969226fd3 | ||
|
0c856438fa | ||
|
e44bb30996 | ||
|
7440086ef1 | ||
|
ef3acb8c43 | ||
|
ee38671400 | ||
|
5b8cd68cf3 | ||
|
53decfd47b | ||
|
65264e3ef5 | ||
|
4ca0fc7a4d | ||
|
7b294478e4 | ||
|
04f0ca7846 | ||
|
61a44101a2 | ||
|
924dfa19cf | ||
|
9ff6ae81bd | ||
|
c33e5c8a17 | ||
|
6129bbc9ab | ||
|
b34b10c6b8 | ||
|
ad106bd884 | ||
|
37fe25ac06 | ||
|
0e0c1dcdc5 | ||
|
c9770eea2f | ||
|
80d2d9d258 | ||
|
3ca1ce4636 | ||
|
8ec91cddab | ||
|
470a576441 | ||
|
33a778873a | ||
|
cf7ca5bd28 | ||
|
a77bce7b37 | ||
|
8e985eb0db | ||
|
915e38f636 | ||
|
e3b1053c03 | ||
|
c2520bff12 | ||
|
cd5bcc3673 | ||
|
254f021903 | ||
|
8fedd2d5f1 | ||
|
cb1830d747 | ||
|
68c47a3238 | ||
|
e644772731 | ||
|
11f1482818 | ||
|
a7decdb62d | ||
|
005b9b595c | ||
|
e6a9d0b090 | ||
|
82879a129e | ||
|
9f66c85281 | ||
|
0f5731360b | ||
|
3fd9e021fa | ||
|
4c3af7bf36 | ||
|
28e7009b49 | ||
|
dd983c803b | ||
|
1b804e61cb | ||
|
02eb3cb6b5 | ||
|
c5d84b4f24 | ||
|
ae88252cb1 | ||
|
3c3d787a2b | ||
|
6aee4fc464 | ||
|
4ef337f1e9 | ||
|
017f6b22f0 | ||
|
602168bc48 | ||
|
fdf384b809 | ||
|
284880d096 | ||
|
a446b37c1f | ||
|
ad75d137b0 | ||
|
d2f4c43526 | ||
|
6bc484617e | ||
|
9d5b7de1d8 | ||
|
a01c370d63 | ||
|
fd5da2de3a | ||
|
3c9f96d621 | ||
|
693cc103ea | ||
|
5ccde61ae1 | ||
|
b96686e6ad | ||
|
e7695aef78 | ||
|
5bb78eb77f | ||
|
5fbf454652 | ||
|
d098eca69d | ||
|
120943a8b3 | ||
|
1e64542f14 | ||
|
e15a867106 | ||
|
349e6ca98f | ||
|
da8669c826 | ||
|
59837bbb90 | ||
|
20c14a0a00 | ||
|
06fdfcdb23 | ||
|
cf48bbc176 | ||
|
40f5d26945 | ||
|
18ea6c4f65 | ||
|
7a661747c5 | ||
|
177a642afc | ||
|
161c8bcf9e | ||
|
e3f8aedd5a | ||
|
7fdbf40cd2 | ||
|
5ea03fad87 | ||
|
dd5da56695 | ||
|
0e1e57c1c3 | ||
|
6ddd6ed0e3 | ||
|
b80a992fdb | ||
|
20de02dffb | ||
|
a3a85ea49f | ||
|
4560033e66 | ||
|
11c61d42dc | ||
|
d1be221d7a | ||
|
cd0294b1b6 | ||
|
0dbe82c781 | ||
|
ad9ef81a77 | ||
|
b36ca92dd9 | ||
|
c8468c29f1 | ||
|
3c40010aff | ||
|
63238b388d | ||
|
809da49301 | ||
|
6b14f38cfa | ||
|
e1e1c20dbe | ||
|
b0360b83d4 | ||
|
f7881651c5 | ||
|
d71224b40b | ||
|
241c4ad857 | ||
|
87661eb85a | ||
|
ad17eb1386 | ||
|
0c631a4990 | ||
|
9b0d85bf6c | ||
|
3b2362c784 | ||
|
aa2370b381 | ||
|
3e07100dc2 | ||
|
1949fb1abe | ||
|
28be423e65 | ||
|
9a75232ca4 | ||
|
64da16f58f | ||
|
fd9510e18f | ||
|
4be9b03ac6 | ||
|
2761d27aaa | ||
|
6d154b1e4f | ||
|
bbb69482e1 | ||
|
f4e344f686 | ||
|
395a840fc4 | ||
|
6580f5771f | ||
|
b21bcc2d45 | ||
|
4481c54376 | ||
|
7b242bf118 | ||
|
f2a478288a | ||
|
01e04e31bf | ||
|
cbc114608b | ||
|
63987f952e | ||
|
9f42306f79 | ||
|
d61bfd7caf | ||
|
13943f77f7 | ||
|
1c94ecdcdf | ||
|
fb83a07f84 | ||
|
3e2d7d76b9 | ||
|
7a0915964a | ||
|
aef97c5563 | ||
|
4c9331c4e9 | ||
|
6fb5552d57 | ||
|
bdb55ef881 | ||
|
628a3bc16c | ||
|
c77396dbdb | ||
|
5002692bda | ||
|
71bb8ed975 | ||
|
6d011ebe32 | ||
|
f1ab34e27c | ||
|
6d655ff757 | ||
|
63627c81eb | ||
|
5dc688dc2e | ||
|
08fb2fe467 | ||
|
f1afeac0bc | ||
|
f75d632740 | ||
|
b26daf8824 | ||
|
393fc14630 | ||
|
c7707dc50e | ||
|
37199a10bf | ||
|
b950370f12 | ||
|
598e4516b3 | ||
|
cd8392bae2 | ||
|
ae7df4fb7f | ||
|
8bee5accb7 | ||
|
cf024b0e61 | ||
|
d3f9232a3f | ||
|
9f655e0d41 | ||
|
e7ed130f2a | ||
|
e421eb61bc | ||
|
bc053580ad | ||
|
11c01235ac | ||
|
c49d862fc5 | ||
|
6993e88265 | ||
|
681e9396b3 | ||
|
85ef40d0ff | ||
|
39c0b74250 | ||
|
a9e629aea6 | ||
|
aa11902aa1 | ||
|
c4088bad12 | ||
|
49d3ddb830 | ||
|
6d802063b4 | ||
|
55a1cdb1c7 | ||
|
ed8a54bd2a | ||
|
5bd5b21543 | ||
|
aec980662f | ||
|
aef1dc6eaf | ||
|
bd45bf7407 | ||
|
e3f6cfa2df | ||
|
609f552c8d | ||
|
5763201307 | ||
|
dee7830793 | ||
|
23f8f35354 | ||
|
cccd09fb5c | ||
|
bf6d59cd21 | ||
|
6ef6eab994 | ||
|
ccff333123 | ||
|
891406cc7f | ||
|
a5d767042c | ||
|
9fdc803c14 | ||
|
8798c295e6 | ||
|
7abb407897 | ||
|
78207d48ba | ||
|
70698e6494 | ||
|
adf02e53fd | ||
|
259c370eb9 | ||
|
7261fcccda | ||
|
a4a4503311 | ||
|
2752540330 | ||
|
0b77b78f6a | ||
|
06bec0ad54 | ||
|
f1126c55ca | ||
|
2caf220b18 | ||
|
4d23f35b9d | ||
|
f6fdb12db2 | ||
|
7773deabc0 | ||
|
91ed3a4a5f | ||
|
20145f7a12 | ||
|
883945e3e8 | ||
|
3feea71146 | ||
|
5e32b8e49f | ||
|
08e63e5fab | ||
|
0ec9496d26 | ||
|
29a0989f28 | ||
|
558b18899c | ||
|
6e95fde4ec | ||
|
e691e17efc | ||
|
80ea14bf7f | ||
|
c25cffafc6 | ||
|
8933b41937 | ||
|
7e2f1d729f | ||
|
d6c87ec10e | ||
|
070abd79ce | ||
|
2d01933c28 | ||
|
bf0bb5aa88 | ||
|
1b4d9fc4e9 | ||
|
2b79295240 | ||
|
42eaaa497f | ||
|
96c894ce5b | ||
|
c0214103a9 | ||
|
2b76a97989 | ||
|
9d77052d9c | ||
|
b4981058a2 | ||
|
032aa64195 | ||
|
7c8e8317a8 | ||
|
eb1cfc4cd4 | ||
|
f1e5cccee7 | ||
|
bc2ed763bd | ||
|
a35995b898 | ||
|
b1f46ed830 | ||
|
6c1565a7d4 | ||
|
2ca6b655ad | ||
|
a83a481ac8 | ||
|
65a8b63b3b | ||
|
b20ca36db9 | ||
|
189f92d7e8 | ||
|
cdd4ec6233 | ||
|
ef1bb4e800 | ||
|
c475acd1ea | ||
|
7d50d7ff52 | ||
|
28522f4f90 | ||
|
ec3a227a02 | ||
|
89decf3474 | ||
|
0b2794e843 | ||
|
554dfb5874 | ||
|
9c30fa1da3 | ||
|
e81bd61e24 | ||
|
7a0b54bb38 | ||
|
f060daf8c4 | ||
|
c1976ef599 | ||
|
f16fb4e1e4 | ||
|
5da2c82f47 | ||
|
d443245d66 | ||
|
9be3eea5fd | ||
|
07a9fd061d | ||
|
2a070c0b1e | ||
|
7b5106d206 | ||
|
821d9cdb02 | ||
|
28575936d3 | ||
|
83a04da4a0 | ||
|
0894b1394f | ||
|
eb33d3c991 | ||
|
d7f01abf3a | ||
|
80635343ae | ||
|
2b38b4e022 | ||
|
4ecde9fc39 | ||
|
445ee274c5 | ||
|
f2bdc514e8 | ||
|
5afff31f72 | ||
|
2dfafa387b | ||
|
7318f4f5dd | ||
|
175b77fe6f | ||
|
346652e508 | ||
|
f0eb42e72d | ||
|
37100f0937 | ||
|
ac980a4dbf | ||
|
a8b53499af | ||
|
a8aeae329e | ||
|
52911539b8 | ||
|
3026ff241b | ||
|
2466a079d5 | ||
|
ed9fdf49e2 | ||
|
668d962233 | ||
|
996f770935 | ||
|
041a6dd919 | ||
|
dbad60d03b | ||
|
27a60423dc | ||
|
5a37d38a84 | ||
|
6f566e67d5 | ||
|
dd490f2ac9 | ||
|
5409af0a6c | ||
|
0ed0d903cc | ||
|
85be4c492d | ||
|
c06ad8b87e | ||
|
b89acb5853 | ||
|
7890511a53 | ||
|
3aa4e6eb93 | ||
|
f8eb9f94f4 | ||
|
c581b9eeb9 | ||
|
ffd9c6995a | ||
|
ef600c0956 | ||
|
5c0a43e8d6 | ||
|
8e332dba30 | ||
|
cd07027192 | ||
|
da2b30268a | ||
|
1163aa4e4e | ||
|
ddb856edc7 | ||
|
9c426bc216 | ||
|
382852d0bd | ||
|
87ae86e1be | ||
|
9547311d7d | ||
|
1613d561c1 | ||
|
538478cac8 | ||
|
267ecce958 | ||
|
fae43fedfa | ||
|
c447022092 | ||
|
56042ad0b6 | ||
|
45da036789 | ||
|
b47b702a52 | ||
|
869424cd16 | ||
|
b9fd01315b | ||
|
a72098b862 | ||
|
86016de6cb | ||
|
592b9fedb9 | ||
|
d06984e3a3 | ||
|
6b55ee250d | ||
|
10eef282fa | ||
|
f312936629 | ||
|
d53bb4c337 | ||
|
1a605e27bc | ||
|
08ee858f64 | ||
|
af70fe3e7e | ||
|
29c5c0af50 | ||
|
9420b750d2 | ||
|
6f5328f663 | ||
|
90214d02d7 | ||
|
2f07f226b8 | ||
|
a8ad19a89d | ||
|
57c07250fd | ||
|
4a3e4a7c5c | ||
|
c284a23afb | ||
|
fad1449de3 | ||
|
f18d161eaf | ||
|
88054b453a | ||
|
c560373596 | ||
|
d698d03521 | ||
|
d8c8d7c588 | ||
|
9120e82517 | ||
|
e214746536 | ||
|
142396400c | ||
|
51d48bdde6 | ||
|
44b055c019 | ||
|
790d7b9170 | ||
|
d8719ceee9 | ||
|
71ddb16574 | ||
|
2932ed670f | ||
|
ae2a6a3d4f | ||
|
30061ada58 | ||
|
a131e28b60 | ||
|
8c1662cfdb | ||
|
299e52e877 | ||
|
95b253db09 | ||
|
067cb2452e | ||
|
45e4092335 | ||
|
7659a997cf | ||
|
aa5e428222 | ||
|
319e4360c8 | ||
|
f5c6e80dbb | ||
|
7108993936 | ||
|
b6553bdc34 | ||
|
19fe689969 | ||
|
d6386cef41 | ||
|
b88f8ae9d2 | ||
|
408c7b2ca6 | ||
|
271253fd0b | ||
|
5348154c42 | ||
|
e1b1f4f3fc | ||
|
75a2110626 | ||
|
9857d3d6ea | ||
|
836a2649d3 | ||
|
59cba2533c | ||
|
a6ac2fbc9a | ||
|
3da8677e32 | ||
|
4d0d7d5ad6 | ||
|
8c4ece4b2d | ||
|
bf3bb8a378 | ||
|
cf5e60f8eb | ||
|
7de707c60a | ||
|
5cd11ad8c3 | ||
|
6bba52a2b6 | ||
|
54b476df4e | ||
|
a68f123594 | ||
|
08ad4f96b9 | ||
|
77a3acf5cc | ||
|
dea585e69b | ||
|
879dacfba6 | ||
|
b459234ddc | ||
|
76d2c676fd | ||
|
d5015d37e1 | ||
|
1b71e4cee7 | ||
|
18ef5c6ff9 | ||
|
35e0561950 | ||
|
adab8e3ed8 | ||
|
89dbb4d300 | ||
|
e3f3686b8a | ||
|
9984e983b4 | ||
|
4ebe67ef53 | ||
|
1a11d4153e | ||
|
cd7cf3583e | ||
|
66a180bc36 | ||
|
eb06667455 | ||
|
0ff8966a27 | ||
|
2cc6794db5 | ||
|
0cb4094dd9 | ||
|
edd213343b | ||
|
46ec655db5 | ||
|
769efd9d06 | ||
|
49cb3b6aa7 | ||
|
8ad98b67d2 | ||
|
8a8f1d3205 | ||
|
4a27f0546c | ||
|
727a7e4b2d | ||
|
2b5e8241ab | ||
|
3dc4fd8dd1 | ||
|
375a27a93d | ||
|
544387d1a0 | ||
|
cb8120d38f | ||
|
78a261f5d3 | ||
|
b8f7653fb2 | ||
|
e0d2a01bc8 | ||
|
560be9f553 | ||
|
47723042c5 | ||
|
d04d676d2f | ||
|
3435636ca0 | ||
|
2e1572d7cc | ||
|
938339690e | ||
|
dbb2c523c1 | ||
|
0b9d436753 | ||
|
2d03f3ce1e | ||
|
c4a476d0d2 | ||
|
5122aed332 | ||
|
5336c5b46e | ||
|
22615f5981 | ||
|
bdf4b4b679 | ||
|
548e300c4b | ||
|
8a5d8c96ef | ||
|
78c2631b6f | ||
|
7c246ffc71 | ||
|
8bb85753cc | ||
|
abfdde28ef | ||
|
9801f1edfa | ||
|
fc3a200a63 | ||
|
6a00658119 | ||
|
353485054e | ||
|
800583b5e2 | ||
|
2db2b7348d | ||
|
f3718257f5 | ||
|
5500762acd | ||
|
4c8f5e1f7a | ||
|
733cf99bb4 | ||
|
58c2f22120 | ||
|
42accebeca | ||
|
1c5c370c12 | ||
|
448645d83a | ||
|
09b6a3b41e | ||
|
74206d60ce | ||
|
c3a0de7fab | ||
|
7edf7a434f | ||
|
b701821550 | ||
|
d022bf2673 | ||
|
7eed8c440c | ||
|
1ab12e380a | ||
|
728e14e8e4 | ||
|
8aa402526a | ||
|
4793ee4786 | ||
|
a09d6c0470 | ||
|
9e83130bd8 | ||
|
2ed01af723 | ||
|
afc80d6a7c | ||
|
532a1b1aba | ||
|
65062b4bcb | ||
|
c16206d816 | ||
|
185283f864 | ||
|
7d1f5c7383 | ||
|
945afc71ef | ||
|
818fe50f77 | ||
|
6fddad7a77 | ||
|
38d131be37 | ||
|
aeff846e1f | ||
|
6b52fc1e2d | ||
|
0671b530ba | ||
|
207f9c26ae | ||
|
6367ce5e5e | ||
|
ba1a2e9942 | ||
|
7f998ecdbd | ||
|
ecd5414287 | ||
|
6107f5f3d2 | ||
|
13afa9f476 | ||
|
cd87c7e88e | ||
|
ed4dea8686 | ||
|
808177f8c9 | ||
|
aed51251b3 | ||
|
1c2730163d | ||
|
0de86dfe6f | ||
|
7a1b99be46 | ||
|
9b64b0139c | ||
|
0a6160d7cf | ||
|
e51a6d332e | ||
|
a9d2741e6a | ||
|
12bd7268d2 | ||
|
be0a23d9ad | ||
|
458a0e608a | ||
|
32f3a50def | ||
|
7de4226d80 | ||
|
6a39c8fc13 | ||
|
dc39669321 | ||
|
be4f27028c | ||
|
60e73e2d1f | ||
|
e8f284d377 | ||
|
3ea3b0bf2e | ||
|
e1a43d2e7d | ||
|
2e918fe1d6 | ||
|
601309c7cc | ||
|
10ddeeb799 | ||
|
3463d6c752 | ||
|
8acce011b5 | ||
|
fe9ea50356 | ||
|
e6f29ae57f | ||
|
6cfd2c510b | ||
|
430ff80198 | ||
|
230fa76d57 | ||
|
46a4b0e0b6 | ||
|
5b3cadb7a8 | ||
|
3153071a8a | ||
|
bba7372556 | ||
|
9fe1a7e2ae | ||
|
98822a39d9 | ||
|
a2c830b908 | ||
|
bdef2cfdfb | ||
|
f229a5e2ec | ||
|
845e061382 | ||
|
e7d4eb1ae3 | ||
|
b4ba56bfb4 | ||
|
25784d1fe5 | ||
|
619eca7a51 | ||
|
f3d85655a0 | ||
|
9600675677 | ||
|
ce8a759192 | ||
|
88bc0bf613 | ||
|
b508e4208a | ||
|
c74d8cf499 | ||
|
a34c2b082f | ||
|
ad49a02879 | ||
|
e985ffc690 | ||
|
6cbb02f02d | ||
|
c0d0ff66b6 | ||
|
1e4d7f8c6e | ||
|
a8a761aa5f | ||
|
41952f0215 | ||
|
bfcc883f01 | ||
|
39722055f5 | ||
|
f85dfa90b8 | ||
|
0a4163d236 | ||
|
78de11a9e3 | ||
|
d2fc6d9f44 | ||
|
abf31f4a79 | ||
|
f28dd4f4de | ||
|
55b64899f5 | ||
|
d4aeeadb26 | ||
|
7ce0110158 | ||
|
7c1e55eb7f | ||
|
27542bc81d | ||
|
9ebbfb2d90 | ||
|
701b1ee744 | ||
|
0edc981cd2 | ||
|
da5942b398 | ||
|
709de81814 | ||
|
90b312a56e | ||
|
459759bfe5 | ||
|
00817aacfe | ||
|
e306eb0874 | ||
|
33a02b47d5 | ||
|
f0a5557e60 | ||
|
58a871c8cc | ||
|
4f56071786 | ||
|
f8b2c79aef | ||
|
8f00d34b0b | ||
|
6129519e5a | ||
|
593091a5e3 | ||
|
22ed163c8f | ||
|
93e2b88d41 | ||
|
7cd54dc8f0 | ||
|
ccd7c8df53 | ||
|
5b3bd3f470 | ||
|
bf1b7f44b6 | ||
|
538dd60580 | ||
|
f453236840 | ||
|
bfe7aa1ed2 | ||
|
9e2ef82902 | ||
|
9352e249ee | ||
|
3800065230 | ||
|
ebc2c4f73a | ||
|
f057440cc1 | ||
|
506f9cfca8 | ||
|
8a70c3353f | ||
|
3d8f123e05 | ||
|
a8c8f15e07 | ||
|
21e647017b | ||
|
2a1bb3dc27 | ||
|
55a3094a65 | ||
|
b4490e209b | ||
|
9aa676333c | ||
|
71b23e57ff | ||
|
2c76bc99fc | ||
|
bb06895145 | ||
|
684965f3e5 | ||
|
e621f4e2fa | ||
|
028ea57232 | ||
|
718fa25c10 | ||
|
90c9f28818 | ||
|
cb9c5a35cb | ||
|
fadaefeaef | ||
|
b17b882a3b | ||
|
f0f3afd5f1 | ||
|
42026b49bf | ||
|
151193c4c3 | ||
|
3448751e0e | ||
|
aae011ed83 | ||
|
c95a269460 | ||
|
98c0e5271f | ||
|
f343131802 | ||
|
ea34ba53b9 | ||
|
b8d8cf19d9 | ||
|
c9be4093e7 | ||
|
082eef708f | ||
|
9106fc5b94 | ||
|
918502742d | ||
|
f32f1eeaa5 | ||
|
2d1404d155 | ||
|
a56997e98c | ||
|
ef918078d1 | ||
|
7e61900cf5 | ||
|
e98f90b099 | ||
|
2e127dff1f | ||
|
828db19e02 | ||
|
99aa3f5713 | ||
|
1a568e2961 | ||
|
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 |
.editorconfig
.github
ISSUE_TEMPLATE.md
.gitignoreCODE_OF_CONDUCT.mdCONTRIBUTING.mdREADME.mdISSUE_TEMPLATE
mergify.ymlpull_request_template.mdreadme-images
renovate.jsonrunner-files
workflows
app
build.gradle.ktsproguard-android-optimize.txtproguard-rules.pro
build.gradle.ktssrc
debug
res
mipmap-anydpi-v26
main
AndroidManifest.xmlbaseline-prof.txt
java
eu
kanade
core
data
AndroidDatabaseHandler.ktDatabaseAdapter.ktDatabaseHandler.ktQueryPagingSource.ktTransactionContext.kt
category
chapter
history
manga
source
NoResultsException.ktSourceDataRepositoryImpl.ktSourceMapper.ktSourcePagingSource.ktSourceRepositoryImpl.kt
track
updates
domain
DomainModule.kt
backup
service
base
category
interactor
CreateCategoryWithName.ktDeleteCategory.ktGetCategories.ktRenameCategory.ktReorderCategory.ktResetCategoryFlags.ktSetDisplayModeForCategory.ktSetMangaCategories.ktSetSortModeForCategory.ktUpdateCategory.kt
model
repository
chapter
interactor
GetChapter.ktGetChapterByMangaId.ktSetDefaultChapterSettings.ktSetReadStatus.ktShouldUpdateDbChapter.ktSyncChaptersWithSource.ktSyncChaptersWithTrackServiceTwoWay.ktUpdateChapter.kt
model
repository
download
extension
history
interactor
DeleteAllHistory.ktGetHistory.ktGetNextUnreadChapters.ktRemoveHistoryById.ktRemoveHistoryByMangaId.ktUpsertHistory.kt
model
repository
library
manga
interactor
GetDuplicateLibraryManga.ktGetFavorites.ktGetLibraryManga.ktGetManga.ktGetMangaWithChapters.ktNetworkToLocalManga.ktResetViewerFlags.ktSetMangaChapterFlags.ktSetMangaViewerFlags.ktUpdateManga.kt
model
repository
source
interactor
GetEnabledSources.ktGetLanguagesWithSources.ktGetRemoteManga.ktGetSourcesWithFavoriteCount.ktGetSourcesWithNonLibraryManga.ktSetMigrateSorting.ktToggleLanguage.ktToggleSource.ktToggleSourcePin.kt
model
repository
service
track
interactor
model
repository
service
ui
updates
presentation
browse
BrowseSourceScreen.ktBrowseSourceState.ktExtensionDetailsScreen.ktExtensionDetailsState.ktExtensionFilterScreen.ktExtensionFilterState.ktExtensionsScreen.ktExtensionsState.ktMigrateMangaScreen.ktMigrateMangaState.ktMigrateSourceScreen.ktMigrateSourceState.ktSourceSearchScreen.ktSourcesFilterScreen.ktSourcesFilterState.ktSourcesScreen.ktSourcesState.kt
components
category
components
AppBar.ktBadges.ktBanners.ktButton.ktChangeCategoryDialog.ktChapterDownloadIndicator.ktCommonMangaItem.ktDeleteLibraryMangaDialog.ktDivider.ktDownloadDropdownMenu.ktDropdownMenu.ktDuplicateMangaDialog.ktEmptyScreen.ktFloatingActionButton.ktIconButton.ktLazyGrid.ktLazyList.ktLinkIcon.ktLoadingScreen.ktMangaBottomActionMenu.ktMangaCover.ktPager.ktPill.ktRelativeDateHeader.ktScaffold.ktSurface.ktSwipeRefresh.ktTabbedScreen.ktTabs.ktTwoPanelBox.ktVerticalFastScroller.kt
crash
history
library
manga
more
LogoHeader.ktMoreScreen.kt
settings
PreferenceItem.ktPreferenceModel.ktPreferenceScaffold.ktPreferenceScreen.kt
screen
AboutScreen.ktClearDatabaseScreen.ktCommons.ktLicensesScreen.ktSearchableSettings.ktSettingsAdvancedScreen.ktSettingsAppearanceScreen.ktSettingsBackupScreen.ktSettingsBrowseScreen.ktSettingsDownloadScreen.ktSettingsGeneralScreen.ktSettingsLibraryScreen.ktSettingsMainScreen.ktSettingsReaderScreen.ktSettingsSearchScreen.ktSettingsSecurityScreen.ktSettingsTrackingScreen.kt
widget
theme
updates
util
Constants.ktElevation.ktLazyListState.ktModifier.ktNavigator.ktPaddingValues.ktPreference.ktResources.ktScrollable.ktScrollbar.kt
webview
tachiyomi
App.ktAppInfo.ktAppModule.ktMigrations.kt
annotations
crash
data
backup
AbstractBackupManager.ktAbstractBackupRestore.ktAbstractBackupRestoreValidator.ktBackupConst.ktBackupCreateService.ktBackupCreatorJob.ktBackupFileValidator.ktBackupManager.ktBackupNotifier.ktBackupRestoreService.ktBackupRestorer.kt
full
legacy
models
cache
coil
database
DatabaseHelper.ktDbExtensions.ktDbOpenCallback.ktDbProvider.kt
mappers
CategoryTypeMapping.ktChapterTypeMapping.ktHistoryTypeMapping.ktMangaCategoryTypeMapping.ktMangaTypeMapping.ktTrackTypeMapping.kt
models
Category.ktCategoryImpl.ktChapter.ktHistory.ktHistoryImpl.ktLibraryManga.ktManga.ktMangaCategory.ktMangaChapter.ktMangaChapterHistory.ktMangaImpl.ktTrack.ktTrackImpl.kt
queries
CategoryQueries.ktChapterQueries.ktHistoryQueries.ktMangaCategoryQueries.ktMangaQueries.ktRawQueries.ktTrackQueries.kt
resolvers
ChapterBackupPutResolver.ktChapterKnownBackupPutResolver.ktChapterProgressPutResolver.ktChapterSourceOrderPutResolver.ktHistoryLastReadPutResolver.ktLibraryMangaGetResolver.ktMangaChapterGetResolver.ktMangaChapterHistoryGetResolver.ktMangaCoverLastModifiedPutResolver.ktMangaFavoritePutResolver.ktMangaFlagsPutResolver.ktMangaLastUpdatedPutResolver.ktMangaNextUpdatedPutResolver.ktMangaTitlePutResolver.kt
tables
download
DownloadCache.ktDownloadManager.ktDownloadNotifier.ktDownloadPendingDeleter.ktDownloadProvider.ktDownloadService.ktDownloadStore.ktDownloader.kt
model
library
notification
preference
saver
track
EnhancedTrackService.ktTrackManager.ktTrackService.kt
anilist
bangumi
Avatar.ktBangumi.ktBangumiApi.ktBangumiInterceptor.ktBangumiModels.ktCollection.ktOAuth.ktStatus.ktUser.kt
job
kitsu
komga
mangaupdates
model
myanimelist
shikimori
updater
extension
glance
network
source
ui
base
activity
controller
BaseController.ktComposeController.ktConductorExtensions.ktDialogController.ktFabController.ktNoToolbarElevationController.ktNucleusController.ktOneWayFadeChangeHandler.ktRxController.ktSearchableNucleusController.ktTabbedController.ktToolbarLiftOnScrollController.kt
delegate
presenter
browse
BrowseController.ktBrowsePresenter.kt
extension
ExtensionAdapter.ktExtensionController.ktExtensionFilterController.ktExtensionFilterPresenter.ktExtensionGroupHolder.ktExtensionGroupItem.ktExtensionHolder.ktExtensionItem.ktExtensionPresenter.ktExtensionTrustDialog.ktExtensionViewUtils.ktExtensionsPresenter.ktExtensionsTab.kt
details
migration
MigrationFlags.kt
manga
MigrateMangaPresenter.ktMigrationMangaAdapter.ktMigrationMangaController.ktMigrationMangaHolder.ktMigrationMangaItem.ktMigrationMangaPresenter.kt
search
sources
source
LangHolder.ktLangItem.ktSourceAdapter.ktSourceController.ktSourceFilterController.ktSourceHolder.ktSourceItem.ktSourcePresenter.ktSourcesFilterController.ktSourcesFilterPresenter.ktSourcesPresenter.ktSourcesTab.kt
browse
BrowseSourceController.ktBrowseSourcePresenter.ktNoResultsException.ktPager.ktProgressItem.ktSourceComfortableGridHolder.ktSourceFilterSheet.ktSourceGridHolder.ktSourceHolder.ktSourceItem.ktSourceListHolder.ktSourcePager.kt
filter
globalsearch
GlobalSearchAdapter.ktGlobalSearchCardAdapter.ktGlobalSearchCardHolder.ktGlobalSearchCardItem.ktGlobalSearchController.ktGlobalSearchHolder.ktGlobalSearchItem.ktGlobalSearchPresenter.kt
latest
category
CategoryAdapter.ktCategoryController.ktCategoryCreateDialog.ktCategoryHolder.ktCategoryItem.ktCategoryPresenter.ktCategoryRenameDialog.kt
download
DownloadAdapter.ktDownloadController.ktDownloadHeaderHolder.ktDownloadHeaderItem.ktDownloadHolder.ktDownloadItem.ktDownloadPresenter.kt
library
ChangeMangaCategoriesDialog.ktChangeMangaCoverDialog.ktDeleteLibraryMangasDialog.ktLibraryAdapter.ktLibraryCategoryAdapter.ktLibraryCategoryView.ktLibraryComfortableGridHolder.ktLibraryCompactGridHolder.ktLibraryController.ktLibraryHolder.ktLibraryItem.ktLibraryListHolder.ktLibraryMangaEvent.ktLibraryPresenter.ktLibrarySelectionEvent.ktLibrarySettingsSheet.ktLibrarySort.kt
setting
main
manga
MangaController.ktMangaPresenter.kt
chapter
ChapterDownloadView.ktChapterHolder.ktChapterItem.ktChaptersAdapter.ktChaptersSettingsSheet.ktDeleteChaptersDialog.ktDownloadCustomChaptersDialog.ktMangaChaptersHeaderAdapter.ktSetChapterSettingsDialog.kt
base
info
track
more
AboutController.ktAboutLinksPreference.ktMoreController.ktMoreHeaderPreference.ktMorePresenter.ktNewUpdateDialogController.kt
reader
PageIndicatorTextView.ktReaderActivity.ktReaderColorFilterView.ktReaderNavigationOverlayView.ktReaderPageSheet.ktReaderPresenter.ktReaderSeekBar.ktReaderSlider.ktSaveImageNotifier.kt
loader
ChapterLoader.ktDownloadPageLoader.ktEpubPageLoader.ktHttpPageLoader.ktRarPageLoader.ktZipPageLoader.kt
model
setting
OrientationType.ktReaderColorFilterSettings.ktReaderGeneralSettings.ktReaderPreferences.ktReaderReadingModeSettings.ktReaderSettingsSheet.ktReadingModeType.kt
viewer
GestureDetectorWithLongTap.ktMissingChapters.ktReaderButton.ktReaderPageImageView.ktReaderProgressIndicator.ktReaderTransitionView.ktViewerConfig.ktViewerNavigation.kt
navigation
pager
Pager.ktPagerButton.ktPagerConfig.ktPagerPageHolder.ktPagerTransitionHolder.ktPagerViewer.ktPagerViewerAdapter.kt
webtoon
recent
DateSectionItem.kt
history
HistoryAdapter.ktHistoryController.ktHistoryHolder.ktHistoryItem.ktHistoryPresenter.ktRemoveHistoryDialog.kt
updates
security
setting
SettingsAdvancedController.ktSettingsBackupController.ktSettingsBrowseController.ktSettingsController.ktSettingsDownloadController.ktSettingsGeneralController.ktSettingsLibraryController.ktSettingsMainController.ktSettingsReaderController.ktSettingsSecurityController.ktSettingsTrackingController.kt
search
SettingsSearchAdapter.ktSettingsSearchController.ktSettingsSearchHelper.ktSettingsSearchHolder.ktSettingsSearchItem.ktSettingsSearchPresenter.kt
track
webview
util
CrashLogUtil.ktMangaExtensions.kt
chapter
ChapterRecognition.ktChapterSettingsHelper.ktChapterSorter.ktChapterSourceSync.ktChapterTrackSync.kt
lang
CloseableExtensions.ktCoroutinesExtensions.ktDateExtensions.ktHash.ktRetryWithDelay.ktRxExtensions.ktStringExtensions.kt
preference
storage
system
AnimationExtensions.ktAuthenticatorUtil.ktBooleanExtensions.ktBuildConfig.ktContextExtensions.ktDeviceUtilExtensions.ktGLUtil.ktImageUtil.ktIntentExtensions.ktInternalResourceHelper.ktLocaleHelper.ktNotificationExtensions.kt
view
widget
ActionToolbar.ktAutofitRecyclerView.ktDialogCustomDownloadView.ktElevationAppBarLayout.ktEmptyView.ktExtendedNavigationView.ktHideBottomNavigationOnScrollBehavior.ktMaterialFastScroll.ktMaterialSpinnerView.ktMinMaxNumberPicker.ktNegativeSeekBar.ktOutlineSpan.ktRecyclerViewPagerAdapter.ktRevealAnimationView.ktSimpleNavigationView.ktStateImageViewTarget.ktTachiyomiAppBarLayout.ktTachiyomiBottomNavigationView.ktTachiyomiChangeHandlerFrameLayout.ktTachiyomiCoordinatorLayout.ktTachiyomiFullscreenDialog.ktTachiyomiScrollingViewBehavior.ktTachiyomiSearchView.ktTachiyomiTextInputEditText.ktThemedSwipeRefreshLayout.kt
listener
materialdialogs
MaterialAlertDialogBuilderExtensions.ktQuadStateMultiChoiceDialogAdapter.ktQuadStateMultiChoiceViewHolder.ktQuadStateTextView.kt
preference
AdaptiveTitlePreferenceCategory.ktIntListPreference.ktLoginDialogPreference.ktLoginPreference.ktSwitchPreferenceCategory.ktSwitchSettingsPreference.kt
sheet
res
anim
color-night-v31
color-v31
color
button_action_selector.xmllibrary_item_foreground.xmlnav_selector.xmlripple_toolbar_fainter.xmlslider_active_track.xmlslider_inactive_track.xmlsource_comfortable_item_title.xmltabs_selector.xml
drawable-nodpi
drawable-v26
drawable
anim_caret_down.xmlanim_library_enter.xmlanim_library_leave.xmlappwidget_background.xmlappwidget_cover_error.xmlcover_error.xmlgradient_shape.xmlic_arrow_back_24dp.xmlic_arrow_downward_24dp.xmlic_broken_image_grey_24dp.xmlic_check_box_24dp.xmlic_check_circle_24dp.xmlic_chevron_left_black_24dp.xmlic_chevron_left_double_black_24dp.xmlic_chevron_right_black_24dp.xmlic_chevron_right_double_black_24dp.xmlic_chrome_reader_mode_24dp.xmlic_cloud_off_24dp.xmlic_code_24dp.xmlic_done_24dp.xmlic_done_green_24dp.xmlic_done_outline_24dp.xmlic_download_chapter_24dp.xmlic_edit_24dp.xmlic_error_outline_24dp.xmlic_favorite_24dp.xmlic_favorite_border_24dp.xmlic_filter_list_24dp.xmlic_flip_to_back_24dp.xmlic_get_app_24dp.xmlic_help_24dp.xmlic_indeterminate_check_box_24dp.xmlic_label_24dp.xmlic_library_outline_24dp.xmlic_offline_pin_24dp.xmlic_public_24dp.xmlic_push_pin_24dp.xmlic_push_pin_outline_24dp.xmlic_save_24dp.xmlic_screen_lock_rotation_24dp.xmlic_security_24dp.xmlic_select_all_24dp.xmlic_settings_backup_restore_24dp.xmlic_sort_24dp.xmlic_sync_24dp.xmlic_tachi_monochrome_launcher.xmlic_translate_24dp.xmlic_tune_24dp.xmlic_view_module_24dp.xmlic_webview_24dp.xmllibrary_item_selector_overlay.xmllist_item_selector_background.xmlmanga_info_gradient.xmlmanga_info_more_gradient.xmlmaterial_bubble_drawable.xmlreader_seekbar_background.xmlreader_seekbar_button.xmlreader_seekbar_ripple.xmlsc_collections_bookmark_48dp.xmlsc_explore_48dp.xmlsc_history_48dp.xmlsc_new_releases_48dp.xmltab_indicator.xmltransparent_tabs_background.xml
font
layout-sw720dp
layout
action_toolbar.xmlappwidget_loading.xmlcategories_controller.xmlchapter_download_view.xmlchapters_item.xmlcommon_dialog_with_checkbox.xmlcommon_tabbed_sheet.xmlcommon_view_empty.xmlcompose_controller.xmldialog_quadstatemultichoice_item.xmldialog_stub_quadstatemultichoice.xmldialog_stub_textinput.xmldownload_controller.xmldownload_custom_amount.xmldownload_header.xmldownload_item.xmldownload_list.xmlextension_card_item.xmlextension_controller.xmlextension_detail_controller.xmlextension_detail_header.xmlglobal_search_controller_card.xmlglobal_search_controller_card_item.xmlhistory_controller.xmlhistory_item.xmllibrary_category.xmllibrary_controller.xmllibrary_grid_recycler.xmllibrary_list_recycler.xmlmain_activity.xmlmain_activity_fab.xmlmain_activity_toolbar.xmlmanga_chapters_header.xmlmanga_controller.xmlmanga_info_header.xmlmigration_manga_controller.xmlnavigation_view_checkbox.xmlnavigation_view_checkedtext.xmlnavigation_view_group.xmlnavigation_view_radio.xmlnavigation_view_spinner.xmlnavigation_view_text.xmlpager_controller.xmlpref_about_links.xmlpref_account_login.xmlpref_library_columns.xmlpref_more_header.xmlpref_settings.xmlpref_spinner.xmlpref_widget_imageview.xmlpref_widget_switch_material.xmlreader_activity.xmlreader_color_filter_settings.xmlreader_error.xmlreader_general_settings.xmlreader_page_sheet.xmlreader_pager_settings.xmlreader_reading_mode_settings.xmlreader_transition_view.xmlreader_webtoon_settings.xmlsection_header_item.xmlsettings_search_controller.xmlsettings_search_controller_card.xmlsource_comfortable_grid_item.xmlsource_compact_grid_item.xmlsource_controller.xmlsource_filter_sheet.xmlsource_list_item.xmlsource_main_controller.xmlsource_main_controller_card_item.xmlsource_progress_item.xmlsource_recycler_autofit.xmltrack_item.xmltrack_search_dialog.xmltrack_search_item.xmlupdates_controller.xmlupdates_item.xmlwebview_activity.xml
menu
browse_extensions.xmlbrowse_migrate.xmlbrowse_sources.xmlcategory_selection.xmlchapter_download.xmlchapter_selection.xmldownload_queue.xmldownload_single.xmlextension_details.xmlgeneric_selection.xmlglobal_search.xmlhistory.xmllibrary.xmllibrary_selection.xmlmanga.xmlmigration.xmlreader.xmlsettings_main.xmlsettings_tracking.xmlsource_browse.xmltrack_search.xmlupdates.xmlupdates_chapter_selection.xmlwebview.xml
mipmap-anydpi-v26
values-es
values-he
values-hu
values-it
values-jv
values-ko
values-lt
values-lv
values-ne
values-night-v31
values-night
bools.xmlcolor_lavender.xmlcolors.xmlcolors_greenapple.xmlcolors_midnightdusk.xmlcolors_strawberry.xmlcolors_tachiyomi.xmlcolors_tako.xmlcolors_tealturqoise.xmlcolors_tidalwave.xmlcolors_yinyang.xmlcolors_yotsuba.xmlthemes.xml
values-ru
values-sk
values-sr
values-sw600dp-port
values-sw720dp
values-te
values-th
values-v26
values-v28
values-v31
values-w820dp
values
arrays.xmlattrs.xmlbools.xmlcolor_lavender.xmlcolors.xmlcolors_appwidget.xmlcolors_greenapple.xmlcolors_midnightdusk.xmlcolors_strawberry.xmlcolors_tachiyomi.xmlcolors_tako.xmlcolors_tealturqoise.xmlcolors_tidalwave.xmlcolors_yinyang.xmlcolors_yotsuba.xmldimens.xmlstyles.xmlthemes.xml
xml
sqldelight
standard
test
java
eu
kanade
tachiyomi
buildSrc
core
.gitignorebuild.gradle.kts
gradle.propertiessrc
main
AndroidManifest.xml
java
eu
kanade
tachiyomi
core
preference
provider
security
network
AndroidCookieJar.ktDohProviders.ktJavaScriptEngine.ktNetworkHelper.ktNetworkPreferences.ktOkHttpExtensions.ktProgressListener.ktProgressResponseBody.ktRequests.kt
interceptor
util
gradle
gradlewgradlew.bati18n
.gitignorebuild.gradle.kts
ktlintCodeStyle.xmlsrc
main
AndroidManifest.xml
res
values-am
values-ar
values-b+es+419
values-be
values-bg
values-bn
values-ca
values-ceb
values-cs
values-cv
values-da
values-de
values-el
values-eo
values-es
values-eu
values-fa
values-fi
values-fil
values-fr
values-gl
values-he
values-hi
values-hr
values-hu
values-in
values-it
values-ja
values-jv
values-ka-rGE
values-kk
values-km
values-kn
values-ko
values-lt
values-lv
values-ml
values-mr
values-ms
values-my
values-nb-rNO
values-ne
values-nl
values-nn
values-or
values-pl
values-pt-rBR
values-pt
values-ro
values-ru
values-sa
values-sah
values-sc
values-sdh
values-si
values-sk
values-sr
values-sv
values-ta
values-te
values-th
values-ti
values-tr
values-uk
values-ur-rPK
values-ur
values-uz
values-vi
values-zh-rCN
values-zh-rTW
values
macrobenchmark
settings.gradle.ktssource-api
7
.editorconfig
Normal file
7
.editorconfig
Normal file
@@ -0,0 +1,7 @@
|
||||
[*.{kt,kts}]
|
||||
indent_size=4
|
||||
insert_final_newline=true
|
||||
ij_kotlin_allow_trailing_comma=true
|
||||
ij_kotlin_allow_trailing_comma_on_call_site=true
|
||||
ij_kotlin_name_count_to_use_star_import = 2147483647
|
||||
ij_kotlin_name_count_to_use_star_import_for_members = 2147483647
|
4
.github/ISSUE_TEMPLATE.md
vendored
4
.github/ISSUE_TEMPLATE.md
vendored
@@ -3,11 +3,11 @@
|
||||
I acknowledge that:
|
||||
|
||||
- I have updated:
|
||||
- To the latest version of the app (stable is v0.11.1)
|
||||
- To the latest version of the app (stable is v0.14.2)
|
||||
- All extensions
|
||||
- I have tried the troubleshooting guide: https://tachiyomi.org/help/guides/troubleshooting-problems/
|
||||
- If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions
|
||||
- I have searched the existing issues and this is new ticket **NOT** a duplicate or related to another open issue
|
||||
- I have searched the existing issues and this is new ticket **NOT** a duplicate or related to another open or closed 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.
|
||||
|
104
.github/ISSUE_TEMPLATE/report_issue.yml
vendored
104
.github/ISSUE_TEMPLATE/report_issue.yml
vendored
@@ -3,57 +3,6 @@ description: Report an issue in Tachiyomi
|
||||
labels: [Bug]
|
||||
body:
|
||||
|
||||
- type: checkboxes
|
||||
id: acknowledgements
|
||||
attributes:
|
||||
label: Acknowledgements
|
||||
description: Read this carefully, we will close and ignore your issue if you skimmed through this.
|
||||
options:
|
||||
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open issue.
|
||||
required: true
|
||||
- label: I have written a short but informative title.
|
||||
required: true
|
||||
- label: If this is an issue with an extension, I should be opening an issue in the [extensions repository](https://github.com/tachiyomiorg/tachiyomi-extensions/issues/new/choose).
|
||||
required: true
|
||||
- label: I have tried the [troubleshooting guide](https://tachiyomi.org/help/guides/troubleshooting/).
|
||||
required: true
|
||||
- label: I have updated the app to version **[0.11.1](https://github.com/tachiyomiorg/tachiyomi/releases/tag/v0.11.1)**.
|
||||
required: true
|
||||
- label: I have updated all installed extensions.
|
||||
required: true
|
||||
- label: I will fill out all of the requested information in this form.
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: tachiyomi-version
|
||||
attributes:
|
||||
label: Tachiyomi version
|
||||
description: You can find your Tachiyomi version in **More → About**.
|
||||
placeholder: |
|
||||
Example: "0.11.1"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: android-version
|
||||
attributes:
|
||||
label: Android version
|
||||
description: You can find this somewhere in your Android settings.
|
||||
placeholder: |
|
||||
Example: "Android 11"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: device
|
||||
attributes:
|
||||
label: Device
|
||||
description: List your device and model.
|
||||
placeholder: |
|
||||
Example: "Google Pixel 5"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: reproduce-steps
|
||||
attributes:
|
||||
@@ -96,7 +45,37 @@ body:
|
||||
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.
|
||||
You can paste the crash logs in plain 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.14.2"
|
||||
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
|
||||
@@ -104,3 +83,24 @@ body:
|
||||
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 or closed 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.14.2](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
|
||||
|
34
.github/ISSUE_TEMPLATE/request_feature.yml
vendored
34
.github/ISSUE_TEMPLATE/request_feature.yml
vendored
@@ -3,23 +3,6 @@ description: Suggest a feature to improve Tachiyomi
|
||||
labels: [Feature request]
|
||||
body:
|
||||
|
||||
- type: checkboxes
|
||||
id: acknowledgements
|
||||
attributes:
|
||||
label: Acknowledgements
|
||||
description: Read this carefully, we will close and ignore your issue if you skimmed through this.
|
||||
options:
|
||||
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open issue.
|
||||
required: true
|
||||
- label: I have written a short but informative title.
|
||||
required: true
|
||||
- label: If this is an issue with an extension, I should be opening an issue in the [extensions repository](https://github.com/tachiyomiorg/tachiyomi-extensions/issues/new/choose).
|
||||
required: true
|
||||
- label: I have updated the app to version **[0.11.1](https://github.com/tachiyomiorg/tachiyomi/releases/tag/v0.11.1)**.
|
||||
required: true
|
||||
- label: I will fill out all of the requested information in this form.
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: feature-description
|
||||
attributes:
|
||||
@@ -37,3 +20,20 @@ body:
|
||||
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 or closed 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.14.2](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**.
|
||||
required: true
|
||||
- label: I will fill out all of the requested information in this form.
|
||||
required: true
|
||||
|
10
.github/mergify.yml
vendored
Normal file
10
.github/mergify.yml
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
#pull_request_rules:
|
||||
# - name: Automatically merge translations
|
||||
# conditions:
|
||||
# - "author = weblate"
|
||||
# - "-conflict"
|
||||
# - "current-day-of-week = Sat"
|
||||
# - "created-at < 1 day ago"
|
||||
# actions:
|
||||
# merge:
|
||||
# method: squash
|
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 ![]() (image error) Size: 454 KiB |
14
.github/renovate.json
vendored
Normal file
14
.github/renovate.json
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"extends": [
|
||||
"config:base"
|
||||
],
|
||||
"schedule": ["every sunday"],
|
||||
"ignoreDeps": [
|
||||
"androidx.core:core-splashscreen",
|
||||
"androidx.work:work-runtime-ktx",
|
||||
"info.android15.nucleus:nucleus-support-v7",
|
||||
"info.android15.nucleus:nucleus",
|
||||
"com.android.tools:r8",
|
||||
"com.google.guava:guava"
|
||||
]
|
||||
}
|
6
.github/runner-files/ci-gradle.properties
vendored
6
.github/runner-files/ci-gradle.properties
vendored
@@ -1,6 +0,0 @@
|
||||
org.gradle.daemon=false
|
||||
org.gradle.jvmargs=-Xmx5120m
|
||||
org.gradle.workers.max=2
|
||||
|
||||
kotlin.incremental=false
|
||||
kotlin.compiler.execution.strategy=in-process
|
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
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Cancel previous runs
|
||||
uses: styfle/cancel-workflow-action@0.5.0
|
||||
with:
|
||||
access_token: ${{ github.token }}
|
||||
|
||||
- name: Clone repo
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Set up JDK 11
|
||||
uses: actions/setup-java@v1
|
||||
with:
|
||||
java-version: 11
|
||||
|
||||
- name: Copy CI gradle.properties
|
||||
run: |
|
||||
mkdir -p ~/.gradle
|
||||
cp .github/runner-files/ci-gradle.properties ~/.gradle/gradle.properties
|
||||
|
||||
- name: Build app
|
||||
uses: eskatos/gradle-command-action@v1
|
||||
with:
|
||||
arguments: assembleStandardRelease
|
||||
wrapper-cache-enabled: true
|
||||
dependencies-cache-enabled: true
|
||||
configuration-cache-enabled: true
|
||||
|
||||
# Sign APK and create release for tags
|
||||
|
||||
- name: Get tag name
|
||||
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'tachiyomiorg/tachiyomi'
|
||||
id: get_tag_name
|
||||
run: |
|
||||
set -x
|
||||
echo "VERSION_TAG=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_ENV
|
||||
|
||||
# TODO: need to support multiple APKs
|
||||
|
||||
- name: Sign APK
|
||||
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'tachiyomiorg/tachiyomi'
|
||||
uses: r0adkll/sign-android-release@v1
|
||||
with:
|
||||
releaseDirectory: app/build/outputs/apk/standard/release
|
||||
signingKeyBase64: ${{ secrets.SIGNING_KEY }}
|
||||
alias: ${{ secrets.ALIAS }}
|
||||
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
|
||||
keyPassword: ${{ secrets.KEY_PASSWORD }}
|
||||
|
||||
- name: Clean up build artifacts
|
||||
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'tachiyomiorg/tachiyomi'
|
||||
run: |
|
||||
cp ${{ env.SIGNED_RELEASE_FILE }} tachiyomi-${{ env.VERSION_TAG }}.apk
|
||||
md5=`md5sum tachiyomi-${{ env.VERSION_TAG }}.apk | awk '{ print $1 }'`
|
||||
echo "APK_MD5=$md5" >> $GITHUB_ENV
|
||||
|
||||
- name: Create Release
|
||||
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'tachiyomiorg/tachiyomi'
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
tag_name: ${{ env.VERSION_TAG }}
|
||||
name: Tachiyomi ${{ env.VERSION_TAG }}
|
||||
body: |
|
||||
MD5: ${{ env.APK_MD5 }}
|
||||
files: |
|
||||
tachiyomi-${{ env.VERSION_TAG }}.apk
|
||||
draft: true
|
||||
prerelease: false
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
39
.github/workflows/build_pull_request.yml
vendored
Normal file
39
.github/workflows/build_pull_request.yml
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
name: PR build check
|
||||
on:
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
- 'i18n/src/main/res/**/strings.xml'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build app
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Clone repo
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Validate Gradle Wrapper
|
||||
uses: gradle/wrapper-validation-action@v1
|
||||
|
||||
- name: Dependency Review
|
||||
uses: actions/dependency-review-action@v2
|
||||
|
||||
- name: Set up JDK 11
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: 11
|
||||
distribution: adopt
|
||||
|
||||
- name: Build app and run unit tests
|
||||
uses: gradle/gradle-command-action@v2
|
||||
with:
|
||||
arguments: assembleStandardRelease testStandardReleaseUnitTest
|
106
.github/workflows/build_push.yml
vendored
Normal file
106
.github/workflows/build_push.yml
vendored
Normal file
@@ -0,0 +1,106 @@
|
||||
name: CI
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
tags:
|
||||
- v*
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build app
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Clone repo
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Validate Gradle Wrapper
|
||||
uses: gradle/wrapper-validation-action@v1
|
||||
|
||||
- name: Set up JDK 11
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: 11
|
||||
distribution: adopt
|
||||
|
||||
- name: Build app and run unit tests
|
||||
uses: gradle/gradle-command-action@v2
|
||||
with:
|
||||
arguments: assembleStandardRelease testStandardReleaseUnitTest
|
||||
|
||||
# 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
|
||||
|
||||
cp app/build/outputs/apk/standard/release/app-standard-x86_64-release-unsigned-signed.apk tachiyomi-x86_64-${{ env.VERSION_TAG }}.apk
|
||||
sha=`sha256sum tachiyomi-x86_64-${{ env.VERSION_TAG }}.apk | awk '{ print $1 }'`
|
||||
echo "APK_X86_64_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 }} |
|
||||
| x86_64 | ${{ env.APK_X86_64_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
|
||||
tachiyomi-x86_64-${{ env.VERSION_TAG }}.apk
|
||||
draft: true
|
||||
prerelease: false
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
32
.github/workflows/issue_closer.yml
vendored
32
.github/workflows/issue_closer.yml
vendored
@@ -1,32 +0,0 @@
|
||||
name: Issue closer
|
||||
on:
|
||||
issues:
|
||||
types: [opened, edited, reopened]
|
||||
|
||||
jobs:
|
||||
autoclose:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Autoclose issues
|
||||
uses: arkon/issue-closer-action@v3.4
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
rules: |
|
||||
[
|
||||
{
|
||||
"type": "body",
|
||||
"regex": ".*DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT.*",
|
||||
"message": "The acknowledgment section was not removed."
|
||||
},
|
||||
{
|
||||
"type": "body",
|
||||
"regex": ".*\\* (Tachiyomi version|Android version|Device): \\?.*",
|
||||
"message": "Requested information in the template was not filled out."
|
||||
},
|
||||
{
|
||||
"type": "both",
|
||||
"regex": ".*(aniyomi|anime).*",
|
||||
"ignoreCase": true,
|
||||
"message": "Tachiyomi does not support anime, and has no plans to support anime. In addition Tachiyomi is not affiliated with Aniyomi https://github.com/jmir1/aniyomi"
|
||||
}
|
||||
]
|
23
.github/workflows/issue_moderator.yml
vendored
23
.github/workflows/issue_moderator.yml
vendored
@@ -1,6 +1,8 @@
|
||||
name: Issue moderator
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened, edited, reopened]
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
@@ -9,6 +11,25 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Moderate issues
|
||||
uses: tachiyomiorg/issue-moderator-action@v1.1
|
||||
uses: tachiyomiorg/issue-moderator-action@v1
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
auto-close-rules: |
|
||||
[
|
||||
{
|
||||
"type": "body",
|
||||
"regex": ".*DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT.*",
|
||||
"message": "The acknowledgment section was not removed."
|
||||
},
|
||||
{
|
||||
"type": "body",
|
||||
"regex": ".*\\* (Tachiyomi version|Android version|Device): \\?.*",
|
||||
"message": "Requested information in the template was not filled out."
|
||||
},
|
||||
{
|
||||
"type": "both",
|
||||
"regex": "^(?!.*myanimelist.*).*(aniyomi|anime).*$",
|
||||
"ignoreCase": true,
|
||||
"message": "Tachiyomi does not support anime, and has no plans to support anime. In addition Tachiyomi is not affiliated with Aniyomi https://github.com/jmir1/aniyomi"
|
||||
}
|
||||
]
|
||||
|
8
.github/workflows/lock.yml
vendored
8
.github/workflows/lock.yml
vendored
@@ -3,7 +3,7 @@ name: Lock threads
|
||||
on:
|
||||
# Daily
|
||||
schedule:
|
||||
- cron: '0 * * * *'
|
||||
- cron: '0 0 * * *'
|
||||
# Manual trigger
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
@@ -12,8 +12,8 @@ jobs:
|
||||
lock:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/lock-threads@v2
|
||||
- uses: dessant/lock-threads@v3
|
||||
with:
|
||||
github-token: ${{ github.token }}
|
||||
issue-lock-inactive-days: '2'
|
||||
pr-lock-inactive-days: '2'
|
||||
issue-inactive-days: '2'
|
||||
pr-inactive-days: '2'
|
||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@@ -11,6 +11,3 @@
|
||||
/build
|
||||
*.apk
|
||||
app/**/output.json
|
||||
|
||||
# Hebrew assets are copied on build
|
||||
app/src/main/res/values-iw/
|
||||
|
@@ -1,76 +1,126 @@
|
||||
# Code of Conduct
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as
|
||||
contributors and maintainers pledge to making participation in our project and
|
||||
our community a harassment-free experience for everyone, regardless of age, body
|
||||
size, disability, ethnicity, sex characteristics, gender identity and expression,
|
||||
level of experience, education, socio-economic status, nationality, personal
|
||||
appearance, race, religion, or sexual identity and orientation.
|
||||
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 creating a positive environment
|
||||
include:
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
* 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 by participants include:
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or
|
||||
advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* 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 electronic
|
||||
address, without explicit permission
|
||||
* 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
|
||||
professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable
|
||||
behavior and are expected to take appropriate and fair corrective action in
|
||||
response to any instances of unacceptable behavior.
|
||||
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.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or
|
||||
reject comments, commits, code, wiki edits, issues, and other contributions
|
||||
that are not aligned to this Code of Conduct, or to ban temporarily or
|
||||
permanently any contributor for other behaviors that they deem inappropriate,
|
||||
threatening, offensive, or harmful.
|
||||
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 both within project spaces and in public spaces
|
||||
when an individual is representing the project or its community. Examples of
|
||||
representing a project or community include using an official project e-mail
|
||||
address, posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event. Representation of a project may be
|
||||
further defined and clarified by project maintainers.
|
||||
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 by contacting the project team at the Tachiyomi [Discord server](https://discord.gg/tachiyomi). All
|
||||
complaints will be reviewed and investigated and will result in a response that
|
||||
is deemed necessary and appropriate to the circumstances. The project team is
|
||||
obligated to maintain confidentiality with regard to the reporter of an incident.
|
||||
Further details of specific enforcement policies may be posted separately.
|
||||
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.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good
|
||||
faith may face temporary or permanent repercussions as determined by other
|
||||
members of the project's leadership.
|
||||
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][homepage], version 1.4,
|
||||
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
|
||||
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).
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
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
|
||||
https://www.contributor-covenant.org/faq
|
||||
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).
|
||||
|
@@ -10,7 +10,23 @@ Thanks for your interest in contributing to Tachiyomi!
|
||||
Pull requests are welcome!
|
||||
|
||||
If you're interested in taking on [an open issue](https://github.com/tachiyomiorg/tachiyomi/issues), please comment on it so others are aware.
|
||||
You do not need to ask for permission nor an assignment.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before you start, please note that the ability to use following technologies is **required** and that existing contributors will not actively teach them to you.
|
||||
|
||||
- Basic [Android development](https://developer.android.com/)
|
||||
- [Kotlin](https://kotlinlang.org/)
|
||||
|
||||
### Tools
|
||||
|
||||
- [Android Studio](https://developer.android.com/studio)
|
||||
- Emulator or phone with developer options enabled to test changes.
|
||||
|
||||
## Getting help
|
||||
|
||||
- Join [the Discord server](https://discord.gg/tachiyomi) for online help and to ask questions while developing.
|
||||
|
||||
# Translations
|
||||
|
||||
@@ -26,7 +42,7 @@ When creating a fork, remember to:
|
||||
- To avoid confusion with the main app:
|
||||
- Change the app name
|
||||
- Change the app icon
|
||||
- Change or disable the [app update checker](https://github.com/tachiyomiorg/tachiyomi/blob/master/app/src/main/java/eu/kanade/tachiyomi/data/updater/GithubUpdateChecker.kt)
|
||||
- Change or disable the [app update checker](https://github.com/tachiyomiorg/tachiyomi/blob/master/app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateChecker.kt)
|
||||
- To avoid installation conflicts:
|
||||
- Change the `applicationId` in [`build.gradle.kts`](https://github.com/tachiyomiorg/tachiyomi/blob/master/app/build.gradle.kts)
|
||||
- To avoid having your data polluting the main app's analytics and crash report services:
|
||||
|
@@ -6,15 +6,13 @@
|
||||
# Tachiyomi
|
||||
Tachiyomi is a free and open source manga reader for Android 6.0 and above.
|
||||
|
||||

|
||||
|
||||
## Features
|
||||
|
||||
Features include:
|
||||
* Online reading from a variety of sources
|
||||
* Local reading of downloaded content
|
||||
* A configurable reader with multiple viewers, reading directions and other settings.
|
||||
* Tracker support: [MyAnimeList](https://myanimelist.net/), [AniList](https://anilist.co/), [Kitsu](https://kitsu.io/), [Shikimori](https://shikimori.one), and [Bangumi](https://bgm.tv/)
|
||||
* Tracker support: [MyAnimeList](https://myanimelist.net/), [AniList](https://anilist.co/), [Kitsu](https://kitsu.io/), [MangaUpdates](https://mangaupdates.com), [Shikimori](https://shikimori.one), and [Bangumi](https://bgm.tv/) support
|
||||
* Categories to organize your library
|
||||
* Light and dark themes
|
||||
* Schedule updating your library for new chapters
|
||||
@@ -38,7 +36,7 @@ Please make sure to read the full guidelines. Your issue may be closed without w
|
||||
|
||||
<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
|
||||
* Preview version is equal to the number of commits as seen in the main page
|
||||
* Include steps to reproduce (if not obvious from description)
|
||||
|
@@ -1,8 +1,5 @@
|
||||
import org.gradle.api.tasks.testing.logging.TestLogEvent
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.TimeZone
|
||||
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
@@ -10,17 +7,19 @@ plugins {
|
||||
kotlin("android")
|
||||
kotlin("plugin.serialization")
|
||||
id("com.github.zellius.shortcut-helper")
|
||||
id("com.squareup.sqldelight")
|
||||
}
|
||||
|
||||
if (gradle.startParameter.taskRequests.toString().contains("Standard")) {
|
||||
apply(plugin = "com.google.gms.google-services")
|
||||
apply<com.google.gms.googleservices.GoogleServicesPlugin>()
|
||||
}
|
||||
|
||||
shortcutHelper.setFilePath("./shortcuts.xml")
|
||||
|
||||
val SUPPORTED_ABIS = setOf("armeabi-v7a", "arm64-v8a", "x86")
|
||||
val SUPPORTED_ABIS = setOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
|
||||
|
||||
android {
|
||||
namespace = "eu.kanade.tachiyomi"
|
||||
compileSdk = AndroidConfig.compileSdk
|
||||
ndkVersion = AndroidConfig.ndk
|
||||
|
||||
@@ -28,14 +27,14 @@ android {
|
||||
applicationId = "eu.kanade.tachiyomi"
|
||||
minSdk = AndroidConfig.minSdk
|
||||
targetSdk = AndroidConfig.targetSdk
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
versionCode = 66
|
||||
versionName = "0.12.0"
|
||||
versionCode = 91
|
||||
versionName = "0.14.2"
|
||||
|
||||
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
||||
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
|
||||
buildConfigField("String", "BUILD_TIME", "\"${getBuildTime()}\"")
|
||||
buildConfigField("boolean", "INCLUDE_UPDATER", "false")
|
||||
buildConfigField("boolean", "PREVIEW", "false")
|
||||
|
||||
// Please disable ACRA or use your own instance in forked versions of the project
|
||||
buildConfigField("String", "ACRA_URI", "\"https://tachiyomi.kanade.eu/crash_report\"")
|
||||
@@ -43,11 +42,13 @@ android {
|
||||
ndk {
|
||||
abiFilters += SUPPORTED_ABIS
|
||||
}
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
splits {
|
||||
abi {
|
||||
isEnable = false
|
||||
isEnable = true
|
||||
reset()
|
||||
include(*SUPPORTED_ABIS.toTypedArray())
|
||||
isUniversalApk = true
|
||||
@@ -58,28 +59,39 @@ android {
|
||||
named("debug") {
|
||||
versionNameSuffix = "-${getCommitCount()}"
|
||||
applicationIdSuffix = ".debug"
|
||||
|
||||
isShrinkResources = true
|
||||
isMinifyEnabled = true
|
||||
proguardFiles("proguard-android-optimize.txt", "proguard-rules.pro")
|
||||
}
|
||||
create("debugFull") { // Debug without R8
|
||||
initWith(getByName("debug"))
|
||||
isShrinkResources = false
|
||||
isMinifyEnabled = false
|
||||
}
|
||||
named("release") {
|
||||
isShrinkResources = true
|
||||
isMinifyEnabled = true
|
||||
proguardFiles("proguard-android-optimize.txt", "proguard-rules.pro")
|
||||
}
|
||||
create("preview") {
|
||||
initWith(getByName("release"))
|
||||
buildConfigField("boolean", "PREVIEW", "true")
|
||||
|
||||
val debugType = getByName("debug")
|
||||
signingConfig = debugType.signingConfig
|
||||
versionNameSuffix = debugType.versionNameSuffix
|
||||
applicationIdSuffix = debugType.applicationIdSuffix
|
||||
matchingFallbacks.add("release")
|
||||
}
|
||||
create("benchmark") {
|
||||
initWith(getByName("release"))
|
||||
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
matchingFallbacks.add("release")
|
||||
isDebuggable = false
|
||||
versionNameSuffix = "-benchmark"
|
||||
applicationIdSuffix = ".benchmark"
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
getByName("debugFull").res.srcDirs("src/debug/res")
|
||||
getByName("preview").res.srcDirs("src/debug/res")
|
||||
getByName("benchmark").res.srcDirs("src/debug/res")
|
||||
}
|
||||
|
||||
flavorDimensions("default")
|
||||
flavorDimensions.add("default")
|
||||
|
||||
productFlavors {
|
||||
create("standard") {
|
||||
@@ -87,18 +99,21 @@ android {
|
||||
dimension = "default"
|
||||
}
|
||||
create("dev") {
|
||||
resConfigs("en", "xxhdpi")
|
||||
resourceConfigurations.addAll(listOf("en", "xxhdpi"))
|
||||
dimension = "default"
|
||||
}
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
exclude("META-INF/DEPENDENCIES")
|
||||
exclude("LICENSE.txt")
|
||||
exclude("META-INF/LICENSE")
|
||||
exclude("META-INF/LICENSE.txt")
|
||||
exclude("META-INF/NOTICE")
|
||||
exclude("META-INF/*.kotlin_module")
|
||||
resources.excludes.addAll(listOf(
|
||||
"META-INF/DEPENDENCIES",
|
||||
"LICENSE.txt",
|
||||
"META-INF/LICENSE",
|
||||
"META-INF/LICENSE.txt",
|
||||
"META-INF/README.md",
|
||||
"META-INF/NOTICE",
|
||||
"META-INF/*.kotlin_module",
|
||||
))
|
||||
}
|
||||
|
||||
dependenciesInfo {
|
||||
@@ -107,12 +122,21 @@ android {
|
||||
|
||||
buildFeatures {
|
||||
viewBinding = true
|
||||
compose = true
|
||||
|
||||
// Disable some unused things
|
||||
aidl = false
|
||||
renderScript = false
|
||||
shaders = false
|
||||
}
|
||||
|
||||
lint {
|
||||
disable("MissingTranslation", "ExtraTranslation")
|
||||
isAbortOnError = false
|
||||
isCheckReleaseBuilds = false
|
||||
abortOnError = false
|
||||
checkReleaseBuilds = false
|
||||
}
|
||||
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion = compose.versions.compiler.get()
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
@@ -123,216 +147,215 @@ android {
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_1_8.toString()
|
||||
}
|
||||
|
||||
sqldelight {
|
||||
database("Database") {
|
||||
packageName = "eu.kanade.tachiyomi"
|
||||
dialect = "sqlite:3.24"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":i18n"))
|
||||
implementation(project(":core"))
|
||||
implementation(project(":source-api"))
|
||||
|
||||
implementation(kotlin("reflect", version = BuildPluginsVersion.KOTLIN))
|
||||
// Compose
|
||||
implementation(platform(compose.bom))
|
||||
implementation(compose.activity)
|
||||
implementation(compose.foundation)
|
||||
implementation(compose.material3.core)
|
||||
implementation(compose.material3.adapter)
|
||||
implementation(compose.material.icons)
|
||||
implementation(compose.animation)
|
||||
implementation(compose.animation.graphics)
|
||||
implementation(compose.ui.tooling)
|
||||
implementation(compose.ui.util)
|
||||
implementation(compose.accompanist.webview)
|
||||
implementation(compose.accompanist.swiperefresh)
|
||||
implementation(compose.accompanist.flowlayout)
|
||||
implementation(compose.accompanist.permissions)
|
||||
|
||||
val coroutinesVersion = "1.5.1"
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion")
|
||||
implementation(androidx.paging.runtime)
|
||||
implementation(androidx.paging.compose)
|
||||
|
||||
// Source models and interfaces from Tachiyomi 1.x
|
||||
implementation("org.tachiyomi:source-api:1.1")
|
||||
implementation(libs.bundles.sqlite)
|
||||
implementation(androidx.sqlite)
|
||||
implementation(libs.sqldelight.android.driver)
|
||||
implementation(libs.sqldelight.coroutines)
|
||||
implementation(libs.sqldelight.android.paging)
|
||||
|
||||
implementation(kotlinx.reflect)
|
||||
|
||||
implementation(platform(kotlinx.coroutines.bom))
|
||||
implementation(kotlinx.bundles.coroutines)
|
||||
|
||||
// AndroidX libraries
|
||||
implementation("androidx.annotation:annotation:1.3.0-alpha01")
|
||||
implementation("androidx.appcompat:appcompat:1.4.0-alpha03")
|
||||
implementation("androidx.biometric:biometric-ktx:1.2.0-alpha03")
|
||||
implementation("androidx.browser:browser:1.3.0")
|
||||
implementation("androidx.cardview:cardview:1.0.0")
|
||||
implementation("androidx.constraintlayout:constraintlayout:2.1.0")
|
||||
implementation("androidx.coordinatorlayout:coordinatorlayout:1.1.0")
|
||||
implementation("androidx.core:core-ktx:1.7.0-alpha01")
|
||||
implementation("androidx.core:core-splashscreen:1.0.0-alpha01")
|
||||
implementation("androidx.preference:preference-ktx:1.1.1")
|
||||
implementation("androidx.recyclerview:recyclerview:1.2.1")
|
||||
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01")
|
||||
implementation(androidx.annotation)
|
||||
implementation(androidx.appcompat)
|
||||
implementation(androidx.biometricktx)
|
||||
implementation(androidx.constraintlayout)
|
||||
implementation(androidx.coordinatorlayout)
|
||||
implementation(androidx.corektx)
|
||||
implementation(androidx.splashscreen)
|
||||
implementation(androidx.recyclerview)
|
||||
implementation(androidx.viewpager)
|
||||
implementation(androidx.glance)
|
||||
implementation(androidx.profileinstaller)
|
||||
|
||||
val lifecycleVersion = "2.4.0-alpha01"
|
||||
implementation("androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion")
|
||||
implementation("androidx.lifecycle:lifecycle-process:$lifecycleVersion")
|
||||
implementation("androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion")
|
||||
implementation(androidx.bundles.lifecycle)
|
||||
|
||||
// Job scheduling
|
||||
implementation("androidx.work:work-runtime-ktx:2.6.0-beta01")
|
||||
implementation(androidx.bundles.workmanager)
|
||||
|
||||
// UI library
|
||||
implementation("com.google.android.material:material:1.5.0-alpha01")
|
||||
|
||||
"standardImplementation"("com.google.firebase:firebase-core:19.0.0")
|
||||
|
||||
// ReactiveX
|
||||
implementation("io.reactivex:rxandroid:1.2.1")
|
||||
implementation("io.reactivex:rxjava:1.3.8")
|
||||
implementation("com.jakewharton.rxrelay:rxrelay:1.2.0")
|
||||
implementation("com.github.pwittchen:reactivenetwork:0.13.0")
|
||||
// RX
|
||||
implementation(libs.bundles.reactivex)
|
||||
implementation(libs.flowreactivenetwork)
|
||||
|
||||
// Network client
|
||||
val okhttpVersion = "4.9.1"
|
||||
implementation("com.squareup.okhttp3:okhttp:$okhttpVersion")
|
||||
implementation("com.squareup.okhttp3:logging-interceptor:$okhttpVersion")
|
||||
implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:$okhttpVersion")
|
||||
implementation("com.squareup.okio:okio:2.10.0")
|
||||
implementation(libs.bundles.okhttp)
|
||||
implementation(libs.okio)
|
||||
|
||||
// TLS 1.3 support for Android < 10
|
||||
implementation("org.conscrypt:conscrypt-android:2.5.2")
|
||||
implementation(libs.conscrypt.android)
|
||||
|
||||
// JSON
|
||||
val kotlinSerializationVersion = "1.2.2"
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinSerializationVersion")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-protobuf:$kotlinSerializationVersion")
|
||||
implementation("com.google.code.gson:gson:2.8.7")
|
||||
implementation("com.github.salomonbrys.kotson:kotson:2.5.0")
|
||||
|
||||
// JavaScript engine
|
||||
implementation("com.squareup.duktape:duktape-android:1.3.0")
|
||||
|
||||
// Disk
|
||||
implementation("com.jakewharton:disklrucache:2.0.2")
|
||||
implementation("com.github.tachiyomiorg:unifile:17bec43")
|
||||
implementation("com.github.junrar:junrar:7.4.0")
|
||||
// Data serialization (JSON, protobuf)
|
||||
implementation(kotlinx.bundles.serialization)
|
||||
|
||||
// HTML parser
|
||||
implementation("org.jsoup:jsoup:1.14.1")
|
||||
implementation(libs.jsoup)
|
||||
|
||||
// Database
|
||||
implementation("androidx.sqlite:sqlite-ktx:2.1.0")
|
||||
implementation("com.github.inorichi.storio:storio-common:8be19de@aar")
|
||||
implementation("com.github.inorichi.storio:storio-sqlite:8be19de@aar")
|
||||
implementation("com.github.requery:sqlite-android:3.36.0")
|
||||
// Disk
|
||||
implementation(libs.disklrucache)
|
||||
implementation(libs.unifile)
|
||||
implementation(libs.junrar)
|
||||
|
||||
// Preferences
|
||||
implementation("com.github.tfcporciuncula.flow-preferences:flow-preferences:1.4.0")
|
||||
implementation(libs.preferencektx)
|
||||
|
||||
// Model View Presenter
|
||||
val nucleusVersion = "3.0.0"
|
||||
implementation("info.android15.nucleus:nucleus:$nucleusVersion")
|
||||
implementation("info.android15.nucleus:nucleus-support-v7:$nucleusVersion")
|
||||
implementation(libs.bundles.nucleus)
|
||||
|
||||
// Dependency injection
|
||||
implementation("com.github.inorichi.injekt:injekt-core:65b0440")
|
||||
implementation(libs.injekt.core)
|
||||
|
||||
// Image library
|
||||
val coilVersion = "1.3.2"
|
||||
implementation("io.coil-kt:coil:$coilVersion")
|
||||
implementation("io.coil-kt:coil-gif:$coilVersion")
|
||||
// Image loading
|
||||
implementation(libs.bundles.coil)
|
||||
|
||||
implementation("com.github.tachiyomiorg:subsampling-scale-image-view:846abe0") {
|
||||
implementation(libs.subsamplingscaleimageview) {
|
||||
exclude(module = "image-decoder")
|
||||
}
|
||||
implementation("com.github.tachiyomiorg:image-decoder:7481a4a")
|
||||
|
||||
// Logging
|
||||
implementation("com.jakewharton.timber:timber:4.7.1")
|
||||
|
||||
// Crash reports
|
||||
implementation("ch.acra:acra-http:5.8.1")
|
||||
implementation(libs.image.decoder)
|
||||
|
||||
// Sort
|
||||
implementation("com.github.gpanther:java-nat-sort:natural-comparator-1.1")
|
||||
implementation(libs.natural.comparator)
|
||||
|
||||
// UI
|
||||
implementation("com.github.dmytrodanylyk.android-process-button:library:1.0.4")
|
||||
implementation("eu.davidea:flexible-adapter:5.1.0")
|
||||
implementation("eu.davidea:flexible-adapter-ui:1.0.0")
|
||||
implementation("com.nightlynexus.viewstatepageradapter:viewstatepageradapter:1.1.0")
|
||||
implementation("com.github.chrisbanes:PhotoView:2.3.0")
|
||||
implementation("com.github.tachiyomiorg:DirectionalViewPager:1.0.0")
|
||||
implementation("dev.chrisbanes.insetter:insetter:0.6.0")
|
||||
// UI libraries
|
||||
implementation(libs.material)
|
||||
implementation(libs.flexible.adapter.core)
|
||||
implementation(libs.flexible.adapter.ui)
|
||||
implementation(libs.photoview)
|
||||
implementation(libs.directionalviewpager) {
|
||||
exclude(group = "androidx.viewpager", module = "viewpager")
|
||||
}
|
||||
implementation(libs.insetter)
|
||||
implementation(libs.markwon)
|
||||
implementation(libs.aboutLibraries.compose)
|
||||
implementation(libs.cascade)
|
||||
implementation(libs.numberpicker)
|
||||
implementation(libs.bundles.voyager)
|
||||
|
||||
// Conductor
|
||||
val conductorVersion = "3.0.0"
|
||||
implementation("com.bluelinelabs:conductor:$conductorVersion")
|
||||
implementation("com.bluelinelabs:conductor-viewpager:$conductorVersion")
|
||||
implementation("com.github.tachiyomiorg:conductor-support-preference:$conductorVersion")
|
||||
implementation(libs.bundles.conductor)
|
||||
|
||||
// FlowBinding
|
||||
val flowbindingVersion = "1.2.0"
|
||||
implementation("io.github.reactivecircus.flowbinding:flowbinding-android:$flowbindingVersion")
|
||||
implementation("io.github.reactivecircus.flowbinding:flowbinding-appcompat:$flowbindingVersion")
|
||||
implementation("io.github.reactivecircus.flowbinding:flowbinding-recyclerview:$flowbindingVersion")
|
||||
implementation("io.github.reactivecircus.flowbinding:flowbinding-swiperefreshlayout:$flowbindingVersion")
|
||||
implementation("io.github.reactivecircus.flowbinding:flowbinding-viewpager:$flowbindingVersion")
|
||||
implementation(libs.bundles.flowbinding)
|
||||
|
||||
// Licenses
|
||||
implementation("com.mikepenz:aboutlibraries:${BuildPluginsVersion.ABOUTLIB_PLUGIN}")
|
||||
// Logging
|
||||
implementation(libs.logcat)
|
||||
|
||||
// Crash reports/analytics
|
||||
implementation(libs.acra.http)
|
||||
"standardImplementation"(libs.firebase.analytics)
|
||||
|
||||
// Shizuku
|
||||
implementation(libs.bundles.shizuku)
|
||||
|
||||
// Tests
|
||||
testImplementation("junit:junit:4.13.2")
|
||||
testImplementation("org.assertj:assertj-core:3.16.1")
|
||||
testImplementation("org.mockito:mockito-core:1.10.19")
|
||||
|
||||
val robolectricVersion = "3.1.4"
|
||||
testImplementation("org.robolectric:robolectric:$robolectricVersion")
|
||||
testImplementation("org.robolectric:shadows-play-services:$robolectricVersion")
|
||||
testImplementation(libs.junit)
|
||||
|
||||
// For detecting memory leaks; see https://square.github.io/leakcanary/
|
||||
// debugImplementation("com.squareup.leakcanary:leakcanary-android:2.7")
|
||||
// debugImplementation(libs.leakcanary.android)
|
||||
implementation(libs.leakcanary.plumber)
|
||||
}
|
||||
|
||||
androidComponents {
|
||||
beforeVariants { variantBuilder ->
|
||||
// Disables standardBenchmark
|
||||
if (variantBuilder.buildType == "benchmark") {
|
||||
variantBuilder.enable = variantBuilder.productFlavors.containsAll(listOf("default" to "dev"))
|
||||
}
|
||||
}
|
||||
onVariants(selector().withFlavor("default" to "standard")) {
|
||||
// Only excluding in standard flavor because this breaks
|
||||
// Layout Inspector's Compose tree
|
||||
it.packaging.resources.excludes.add("META-INF/*.version")
|
||||
}
|
||||
}
|
||||
|
||||
tasks {
|
||||
withType<Test> {
|
||||
useJUnitPlatform()
|
||||
testLogging {
|
||||
events(TestLogEvent.PASSED, TestLogEvent.SKIPPED, TestLogEvent.FAILED)
|
||||
}
|
||||
}
|
||||
|
||||
withType<org.jmailen.gradle.kotlinter.tasks.LintTask>().configureEach {
|
||||
exclude { it.file.path.contains("generated[\\\\/]".toRegex()) }
|
||||
}
|
||||
|
||||
// See https://kotlinlang.org/docs/reference/experimental.html#experimental-status-of-experimental-api(-markers)
|
||||
withType<KotlinCompile> {
|
||||
kotlinOptions.freeCompilerArgs += listOf(
|
||||
"-Xopt-in=kotlin.Experimental",
|
||||
"-Xopt-in=kotlin.RequiresOptIn",
|
||||
"-Xuse-experimental=kotlin.ExperimentalStdlibApi",
|
||||
"-Xuse-experimental=kotlinx.coroutines.FlowPreview",
|
||||
"-Xuse-experimental=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
||||
"-Xuse-experimental=kotlinx.coroutines.InternalCoroutinesApi",
|
||||
"-Xuse-experimental=kotlinx.serialization.ExperimentalSerializationApi",
|
||||
"-Xuse-experimental=coil.annotation.ExperimentalCoilApi",
|
||||
"-opt-in=coil.annotation.ExperimentalCoilApi",
|
||||
"-opt-in=com.google.accompanist.pager.ExperimentalPagerApi",
|
||||
"-opt-in=com.google.accompanist.permissions.ExperimentalPermissionsApi",
|
||||
"-opt-in=androidx.compose.material.ExperimentalMaterialApi",
|
||||
"-opt-in=androidx.compose.material3.ExperimentalMaterial3Api",
|
||||
"-opt-in=androidx.compose.ui.ExperimentalComposeUiApi",
|
||||
"-opt-in=androidx.compose.foundation.ExperimentalFoundationApi",
|
||||
"-opt-in=androidx.compose.animation.ExperimentalAnimationApi",
|
||||
"-opt-in=androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi",
|
||||
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
||||
"-opt-in=kotlinx.coroutines.FlowPreview",
|
||||
"-opt-in=kotlinx.coroutines.InternalCoroutinesApi",
|
||||
"-opt-in=kotlinx.serialization.ExperimentalSerializationApi",
|
||||
)
|
||||
}
|
||||
|
||||
// Duplicating Hebrew string assets due to some locale code issues on different devices
|
||||
val copyHebrewStrings = task("copyHebrewStrings", type = Copy::class) {
|
||||
from("./src/main/res/values-he")
|
||||
into("./src/main/res/values-iw")
|
||||
include("**/*")
|
||||
if (project.findProperty("tachiyomi.enableComposeCompilerMetrics") == "true") {
|
||||
kotlinOptions.freeCompilerArgs += listOf(
|
||||
"-P",
|
||||
"plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=" +
|
||||
project.buildDir.absolutePath + "/compose_metrics"
|
||||
)
|
||||
kotlinOptions.freeCompilerArgs += listOf(
|
||||
"-P",
|
||||
"plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=" +
|
||||
project.buildDir.absolutePath + "/compose_metrics"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
preBuild {
|
||||
dependsOn(formatKotlin, copyHebrewStrings)
|
||||
val ktlintTask = if (System.getenv("GITHUB_BASE_REF") == null) formatKotlin else lintKotlin
|
||||
dependsOn(ktlintTask)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
buildscript {
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath(kotlin("gradle-plugin", version = BuildPluginsVersion.KOTLIN))
|
||||
classpath(kotlinx.gradle)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Git is needed in your system PATH for these commands to work.
|
||||
// If it's not installed, you can return a random value as a workaround
|
||||
fun getCommitCount(): String {
|
||||
return runCommand("git rev-list --count HEAD")
|
||||
// return "1"
|
||||
}
|
||||
|
||||
fun getGitSha(): String {
|
||||
return runCommand("git rev-parse --short HEAD")
|
||||
// return "1"
|
||||
}
|
||||
|
||||
fun getBuildTime(): String {
|
||||
val df = SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'")
|
||||
df.timeZone = TimeZone.getTimeZone("UTC")
|
||||
return df.format(Date())
|
||||
}
|
||||
|
||||
fun runCommand(command: String): String {
|
||||
val byteOut = ByteArrayOutputStream()
|
||||
project.exec {
|
||||
commandLine = command.split(" ")
|
||||
standardOutput = byteOut
|
||||
}
|
||||
return String(byteOut.toByteArray()).trim()
|
||||
}
|
||||
|
@@ -1,4 +1,3 @@
|
||||
-allowaccessmodification
|
||||
-dontusemixedcaseclassnames
|
||||
-verbose
|
||||
|
||||
|
51
app/proguard-rules.pro
vendored
51
app/proguard-rules.pro
vendored
@@ -1,19 +1,25 @@
|
||||
-dontobfuscate
|
||||
|
||||
# Keep extension's common dependencies
|
||||
-keep,allowoptimization class eu.kanade.tachiyomi.** { public protected *; }
|
||||
-keep,allowoptimization class androidx.preference.** { *; }
|
||||
# Keep common dependencies used in extensions
|
||||
-keep,allowoptimization class androidx.preference.** { public protected *; }
|
||||
-keep,allowoptimization class kotlin.** { public protected *; }
|
||||
-keep,allowoptimization class kotlinx.coroutines.** { public protected *; }
|
||||
-keep,allowoptimization class kotlinx.serialization.** { 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 app.cash.quickjs.** { public protected *; }
|
||||
-keep,allowoptimization class uy.kohesive.injekt.** { public protected *; }
|
||||
|
||||
# From extensions-lib
|
||||
-keep,allowoptimization class eu.kanade.tachiyomi.network.interceptor.RateLimitInterceptorKt { public protected *; }
|
||||
-keep,allowoptimization class eu.kanade.tachiyomi.network.interceptor.SpecificHostRateLimitInterceptorKt { public protected *; }
|
||||
-keep,allowoptimization class eu.kanade.tachiyomi.network.NetworkHelper { public protected *; }
|
||||
-keep,allowoptimization class eu.kanade.tachiyomi.network.OkHttpExtensionsKt { public protected *; }
|
||||
-keep,allowoptimization class eu.kanade.tachiyomi.network.RequestsKt { public protected *; }
|
||||
-keep,allowoptimization class eu.kanade.tachiyomi.AppInfo { public protected *; }
|
||||
|
||||
##---------------Begin: proguard configuration for RxJava 1.x ----------
|
||||
-dontwarn sun.misc.**
|
||||
|
||||
@@ -33,30 +39,6 @@
|
||||
-dontnote rx.internal.util.PlatformDependent
|
||||
##---------------End: proguard configuration for RxJava 1.x ----------
|
||||
|
||||
##---------------Begin: proguard configuration for Gson ----------
|
||||
# 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.
|
||||
-keepattributes Signature
|
||||
|
||||
# For using GSON @Expose annotation
|
||||
-keepattributes *Annotation*
|
||||
|
||||
# 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)
|
||||
-keep class * extends com.google.gson.TypeAdapter
|
||||
-keep class * implements com.google.gson.TypeAdapterFactory
|
||||
-keep class * implements com.google.gson.JsonSerializer
|
||||
-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 ----------
|
||||
|
||||
##---------------Begin: proguard configuration for kotlinx.serialization ----------
|
||||
-keepattributes *Annotation*, InnerClasses
|
||||
-dontnote kotlinx.serialization.AnnotationsKt # core serialization annotations
|
||||
@@ -69,11 +51,11 @@
|
||||
kotlinx.serialization.KSerializer serializer(...);
|
||||
}
|
||||
|
||||
-keep,includedescriptorclasses class eu.kanade.tachiyomi.**$$serializer { *; }
|
||||
-keepclassmembers class eu.kanade.tachiyomi.** {
|
||||
-keep,includedescriptorclasses class eu.kanade.**$$serializer { *; }
|
||||
-keepclassmembers class eu.kanade.** {
|
||||
*** Companion;
|
||||
}
|
||||
-keepclasseswithmembers class eu.kanade.tachiyomi.** {
|
||||
-keepclasseswithmembers class eu.kanade.** {
|
||||
kotlinx.serialization.KSerializer serializer(...);
|
||||
}
|
||||
|
||||
@@ -82,3 +64,6 @@
|
||||
<methods>;
|
||||
}
|
||||
##---------------End: proguard configuration for kotlinx.serialization ----------
|
||||
|
||||
# XmlUtil
|
||||
-keep public enum nl.adaptivity.xmlutil.EventType { *; }
|
@@ -2,4 +2,5 @@
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@android:color/transparent"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
<monochrome android:drawable="@drawable/ic_tachi_monochrome_launcher" />
|
||||
</adaptive-icon>
|
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="eu.kanade.tachiyomi">
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<!-- Internet -->
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
@@ -18,23 +18,35 @@
|
||||
<!-- For managing extensions -->
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_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+ -->
|
||||
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
|
||||
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
|
||||
<!-- Remove permission from Firebase dependency -->
|
||||
<uses-permission android:name="com.google.android.gms.permission.AD_ID"
|
||||
tools:node="remove" />
|
||||
|
||||
<application
|
||||
android:name=".App"
|
||||
android:allowBackup="false"
|
||||
android:hardwareAccelerated="true"
|
||||
android:hasFragileUserData="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:largeHeap="true"
|
||||
android:localeConfig="@xml/locales_config"
|
||||
android:requestLegacyExternalStorage="true"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:theme="@style/Theme.Tachiyomi"
|
||||
android:supportsRtl="true"
|
||||
android:networkSecurityConfig="@xml/network_security_config">
|
||||
|
||||
<!-- enable profiling by macrobenchmark -->
|
||||
<profileable
|
||||
android:shell="true"
|
||||
tools:targetApi="q" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.main.MainActivity"
|
||||
android:launchMode="singleTop"
|
||||
@@ -49,6 +61,12 @@
|
||||
android:name="android.app.shortcuts"
|
||||
android:resource="@xml/shortcuts" />
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:process=":error_handler"
|
||||
android:name=".crash.CrashActivity"
|
||||
android:exported="false" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.main.DeepLinkActivity"
|
||||
android:launchMode="singleTask"
|
||||
@@ -168,6 +186,20 @@
|
||||
android:name=".data.notification.NotificationReceiver"
|
||||
android:exported="false" />
|
||||
|
||||
<receiver
|
||||
android:name=".glance.UpdatesGridGlanceReceiver"
|
||||
android:enabled="@bool/glance_appwidget_available"
|
||||
android:exported="false"
|
||||
android:label="@string/label_recent_updates">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.appwidget.provider"
|
||||
android:resource="@xml/updates_grid_glance_widget_info" />
|
||||
</receiver>
|
||||
|
||||
<service
|
||||
android:name=".data.library.LibraryUpdateService"
|
||||
android:exported="false" />
|
||||
@@ -177,17 +209,25 @@
|
||||
android:exported="false" />
|
||||
|
||||
<service
|
||||
android:name=".data.updater.UpdaterService"
|
||||
android:exported="false" />
|
||||
|
||||
<service
|
||||
android:name=".data.backup.BackupCreateService"
|
||||
android:name=".data.updater.AppUpdateService"
|
||||
android:exported="false" />
|
||||
|
||||
<service
|
||||
android:name=".data.backup.BackupRestoreService"
|
||||
android:exported="false" />
|
||||
|
||||
<service android:name=".extension.util.ExtensionInstallService"
|
||||
android:exported="false" />
|
||||
|
||||
<service
|
||||
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
|
||||
android:enabled="false"
|
||||
android:exported="false">
|
||||
<meta-data
|
||||
android:name="autoStoreLocales"
|
||||
android:value="true" />
|
||||
</service>
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.provider"
|
||||
@@ -198,6 +238,26 @@
|
||||
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" />
|
||||
|
||||
<!-- Disable advertising ID collection for Firebase -->
|
||||
<meta-data
|
||||
android:name="google_analytics_adid_collection_enabled"
|
||||
android:value="false" />
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
18768
app/src/main/baseline-prof.txt
Normal file
18768
app/src/main/baseline-prof.txt
Normal file
File diff suppressed because it is too large
Load Diff
55
app/src/main/java/eu/kanade/core/prefs/CheckboxState.kt
Normal file
55
app/src/main/java/eu/kanade/core/prefs/CheckboxState.kt
Normal file
@@ -0,0 +1,55 @@
|
||||
package eu.kanade.core.prefs
|
||||
|
||||
import androidx.compose.ui.state.ToggleableState
|
||||
|
||||
sealed class CheckboxState<T>(open val value: T) {
|
||||
abstract fun next(): CheckboxState<T>
|
||||
|
||||
sealed class State<T>(override val value: T) : CheckboxState<T>(value) {
|
||||
data class Checked<T>(override val value: T) : State<T>(value)
|
||||
data class None<T>(override val value: T) : State<T>(value)
|
||||
|
||||
val isChecked: Boolean
|
||||
get() = this is Checked
|
||||
|
||||
override fun next(): CheckboxState<T> {
|
||||
return when (this) {
|
||||
is Checked -> None(value)
|
||||
is None -> Checked(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
sealed class TriState<T>(override val value: T) : CheckboxState<T>(value) {
|
||||
data class Include<T>(override val value: T) : TriState<T>(value)
|
||||
data class Exclude<T>(override val value: T) : TriState<T>(value)
|
||||
data class None<T>(override val value: T) : TriState<T>(value)
|
||||
|
||||
override fun next(): CheckboxState<T> {
|
||||
return when (this) {
|
||||
is Exclude -> None(value)
|
||||
is Include -> Exclude(value)
|
||||
is None -> Include(value)
|
||||
}
|
||||
}
|
||||
|
||||
fun asState(): ToggleableState {
|
||||
return when (this) {
|
||||
is Exclude -> ToggleableState.Indeterminate
|
||||
is Include -> ToggleableState.On
|
||||
is None -> ToggleableState.Off
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <T> T.asCheckboxState(condition: (T) -> Boolean): CheckboxState.State<T> {
|
||||
return if (condition(this)) {
|
||||
CheckboxState.State.Checked(this)
|
||||
} else {
|
||||
CheckboxState.State.None(this)
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <T> List<T>.mapAsCheckboxState(condition: (T) -> Boolean): List<CheckboxState.State<T>> {
|
||||
return this.map { it.asCheckboxState(condition) }
|
||||
}
|
@@ -0,0 +1,36 @@
|
||||
package eu.kanade.core.prefs
|
||||
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import eu.kanade.tachiyomi.core.preference.Preference
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
|
||||
class PreferenceMutableState<T>(
|
||||
private val preference: Preference<T>,
|
||||
scope: CoroutineScope,
|
||||
) : MutableState<T> {
|
||||
|
||||
private val state = mutableStateOf(preference.get())
|
||||
|
||||
init {
|
||||
preference.changes()
|
||||
.onEach { state.value = it }
|
||||
.launchIn(scope)
|
||||
}
|
||||
|
||||
override var value: T
|
||||
get() = state.value
|
||||
set(value) {
|
||||
preference.set(value)
|
||||
}
|
||||
|
||||
override fun component1(): T {
|
||||
return state.value
|
||||
}
|
||||
|
||||
override fun component2(): (T) -> Unit {
|
||||
return { preference.set(it) }
|
||||
}
|
||||
}
|
16
app/src/main/java/eu/kanade/core/util/ListUtils.kt
Normal file
16
app/src/main/java/eu/kanade/core/util/ListUtils.kt
Normal file
@@ -0,0 +1,16 @@
|
||||
package eu.kanade.core.util
|
||||
|
||||
fun <T : R, R : Any> List<T>.insertSeparators(
|
||||
generator: (T?, T?) -> R?,
|
||||
): List<R> {
|
||||
if (isEmpty()) return emptyList()
|
||||
val newList = mutableListOf<R>()
|
||||
for (i in -1..lastIndex) {
|
||||
val before = getOrNull(i)
|
||||
before?.let { newList.add(it) }
|
||||
val after = getOrNull(i + 1)
|
||||
val separator = generator.invoke(before, after)
|
||||
separator?.let { newList.add(it) }
|
||||
}
|
||||
return newList
|
||||
}
|
61
app/src/main/java/eu/kanade/core/util/RxJavaExtensions.kt
Normal file
61
app/src/main/java/eu/kanade/core/util/RxJavaExtensions.kt
Normal file
@@ -0,0 +1,61 @@
|
||||
package eu.kanade.core.util
|
||||
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineStart
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import rx.Emitter
|
||||
import rx.Observable
|
||||
import rx.Observer
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
fun <T : Any> Observable<T>.asFlow(): Flow<T> = callbackFlow {
|
||||
val observer = object : Observer<T> {
|
||||
override fun onNext(t: T) {
|
||||
trySend(t)
|
||||
}
|
||||
|
||||
override fun onError(e: Throwable) {
|
||||
close(e)
|
||||
}
|
||||
|
||||
override fun onCompleted() {
|
||||
close()
|
||||
}
|
||||
}
|
||||
val subscription = subscribe(observer)
|
||||
awaitClose { subscription.unsubscribe() }
|
||||
}
|
||||
|
||||
fun <T : Any> Flow<T>.asObservable(
|
||||
context: CoroutineContext = Dispatchers.Unconfined,
|
||||
backpressureMode: Emitter.BackpressureMode = Emitter.BackpressureMode.NONE,
|
||||
): Observable<T> {
|
||||
return Observable.create(
|
||||
{ emitter ->
|
||||
/*
|
||||
* ATOMIC is used here to provide stable behaviour of subscribe+dispose pair even if
|
||||
* asObservable is already invoked from unconfined
|
||||
*/
|
||||
val job = GlobalScope.launch(context = context, start = CoroutineStart.ATOMIC) {
|
||||
try {
|
||||
collect { emitter.onNext(it) }
|
||||
emitter.onCompleted()
|
||||
} catch (e: Throwable) {
|
||||
// Ignore `CancellationException` as error, since it indicates "normal cancellation"
|
||||
if (e !is CancellationException) {
|
||||
emitter.onError(e)
|
||||
} else {
|
||||
emitter.onCompleted()
|
||||
}
|
||||
}
|
||||
}
|
||||
emitter.setCancellation { job.cancel() }
|
||||
},
|
||||
backpressureMode,
|
||||
)
|
||||
}
|
90
app/src/main/java/eu/kanade/data/AndroidDatabaseHandler.kt
Normal file
90
app/src/main/java/eu/kanade/data/AndroidDatabaseHandler.kt
Normal file
@@ -0,0 +1,90 @@
|
||||
package eu.kanade.data
|
||||
|
||||
import androidx.paging.PagingSource
|
||||
import com.squareup.sqldelight.Query
|
||||
import com.squareup.sqldelight.db.SqlDriver
|
||||
import com.squareup.sqldelight.runtime.coroutines.asFlow
|
||||
import com.squareup.sqldelight.runtime.coroutines.mapToList
|
||||
import com.squareup.sqldelight.runtime.coroutines.mapToOne
|
||||
import com.squareup.sqldelight.runtime.coroutines.mapToOneOrNull
|
||||
import eu.kanade.tachiyomi.Database
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class AndroidDatabaseHandler(
|
||||
val db: Database,
|
||||
private val driver: SqlDriver,
|
||||
val queryDispatcher: CoroutineDispatcher = Dispatchers.IO,
|
||||
val transactionDispatcher: CoroutineDispatcher = queryDispatcher,
|
||||
) : DatabaseHandler {
|
||||
|
||||
val suspendingTransactionId = ThreadLocal<Int>()
|
||||
|
||||
override suspend fun <T> await(inTransaction: Boolean, block: suspend Database.() -> T): T {
|
||||
return dispatch(inTransaction, block)
|
||||
}
|
||||
|
||||
override suspend fun <T : Any> awaitList(
|
||||
inTransaction: Boolean,
|
||||
block: suspend Database.() -> Query<T>,
|
||||
): List<T> {
|
||||
return dispatch(inTransaction) { block(db).executeAsList() }
|
||||
}
|
||||
|
||||
override suspend fun <T : Any> awaitOne(
|
||||
inTransaction: Boolean,
|
||||
block: suspend Database.() -> Query<T>,
|
||||
): T {
|
||||
return dispatch(inTransaction) { block(db).executeAsOne() }
|
||||
}
|
||||
|
||||
override suspend fun <T : Any> awaitOneOrNull(
|
||||
inTransaction: Boolean,
|
||||
block: suspend Database.() -> Query<T>,
|
||||
): T? {
|
||||
return dispatch(inTransaction) { block(db).executeAsOneOrNull() }
|
||||
}
|
||||
|
||||
override fun <T : Any> subscribeToList(block: Database.() -> Query<T>): Flow<List<T>> {
|
||||
return block(db).asFlow().mapToList(queryDispatcher)
|
||||
}
|
||||
|
||||
override fun <T : Any> subscribeToOne(block: Database.() -> Query<T>): Flow<T> {
|
||||
return block(db).asFlow().mapToOne(queryDispatcher)
|
||||
}
|
||||
|
||||
override fun <T : Any> subscribeToOneOrNull(block: Database.() -> Query<T>): Flow<T?> {
|
||||
return block(db).asFlow().mapToOneOrNull(queryDispatcher)
|
||||
}
|
||||
|
||||
override fun <T : Any> subscribeToPagingSource(
|
||||
countQuery: Database.() -> Query<Long>,
|
||||
queryProvider: Database.(Long, Long) -> Query<T>,
|
||||
): PagingSource<Long, T> {
|
||||
return QueryPagingSource(
|
||||
handler = this,
|
||||
countQuery = countQuery,
|
||||
queryProvider = { limit, offset ->
|
||||
queryProvider.invoke(db, limit, offset)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun <T> dispatch(inTransaction: Boolean, block: suspend Database.() -> T): T {
|
||||
// Create a transaction if needed and run the calling block inside it.
|
||||
if (inTransaction) {
|
||||
return withTransaction { block(db) }
|
||||
}
|
||||
|
||||
// If we're currently in the transaction thread, there's no need to dispatch our query.
|
||||
if (driver.currentTransaction() != null) {
|
||||
return block(db)
|
||||
}
|
||||
|
||||
// Get the current database context and run the calling block.
|
||||
val context = getCurrentDatabaseContext()
|
||||
return withContext(context) { block(db) }
|
||||
}
|
||||
}
|
30
app/src/main/java/eu/kanade/data/DatabaseAdapter.kt
Normal file
30
app/src/main/java/eu/kanade/data/DatabaseAdapter.kt
Normal file
@@ -0,0 +1,30 @@
|
||||
package eu.kanade.data
|
||||
|
||||
import com.squareup.sqldelight.ColumnAdapter
|
||||
import eu.kanade.tachiyomi.source.model.UpdateStrategy
|
||||
import java.util.Date
|
||||
|
||||
val dateAdapter = object : ColumnAdapter<Date, Long> {
|
||||
override fun decode(databaseValue: Long): Date = Date(databaseValue)
|
||||
override fun encode(value: Date): Long = value.time
|
||||
}
|
||||
|
||||
private const val listOfStringsSeparator = ", "
|
||||
val listOfStringsAdapter = object : ColumnAdapter<List<String>, String> {
|
||||
override fun decode(databaseValue: String) =
|
||||
if (databaseValue.isEmpty()) {
|
||||
listOf()
|
||||
} else {
|
||||
databaseValue.split(listOfStringsSeparator)
|
||||
}
|
||||
override fun encode(value: List<String>) = value.joinToString(separator = listOfStringsSeparator)
|
||||
}
|
||||
|
||||
val updateStrategyAdapter = object : ColumnAdapter<UpdateStrategy, Long> {
|
||||
private val enumValues by lazy { UpdateStrategy.values() }
|
||||
|
||||
override fun decode(databaseValue: Long): UpdateStrategy =
|
||||
enumValues.getOrElse(databaseValue.toInt()) { UpdateStrategy.ALWAYS_UPDATE }
|
||||
|
||||
override fun encode(value: UpdateStrategy): Long = value.ordinal.toLong()
|
||||
}
|
37
app/src/main/java/eu/kanade/data/DatabaseHandler.kt
Normal file
37
app/src/main/java/eu/kanade/data/DatabaseHandler.kt
Normal file
@@ -0,0 +1,37 @@
|
||||
package eu.kanade.data
|
||||
|
||||
import androidx.paging.PagingSource
|
||||
import com.squareup.sqldelight.Query
|
||||
import eu.kanade.tachiyomi.Database
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface DatabaseHandler {
|
||||
|
||||
suspend fun <T> await(inTransaction: Boolean = false, block: suspend Database.() -> T): T
|
||||
|
||||
suspend fun <T : Any> awaitList(
|
||||
inTransaction: Boolean = false,
|
||||
block: suspend Database.() -> Query<T>,
|
||||
): List<T>
|
||||
|
||||
suspend fun <T : Any> awaitOne(
|
||||
inTransaction: Boolean = false,
|
||||
block: suspend Database.() -> Query<T>,
|
||||
): T
|
||||
|
||||
suspend fun <T : Any> awaitOneOrNull(
|
||||
inTransaction: Boolean = false,
|
||||
block: suspend Database.() -> Query<T>,
|
||||
): T?
|
||||
|
||||
fun <T : Any> subscribeToList(block: Database.() -> Query<T>): Flow<List<T>>
|
||||
|
||||
fun <T : Any> subscribeToOne(block: Database.() -> Query<T>): Flow<T>
|
||||
|
||||
fun <T : Any> subscribeToOneOrNull(block: Database.() -> Query<T>): Flow<T?>
|
||||
|
||||
fun <T : Any> subscribeToPagingSource(
|
||||
countQuery: Database.() -> Query<Long>,
|
||||
queryProvider: Database.(Long, Long) -> Query<T>,
|
||||
): PagingSource<Long, T>
|
||||
}
|
72
app/src/main/java/eu/kanade/data/QueryPagingSource.kt
Normal file
72
app/src/main/java/eu/kanade/data/QueryPagingSource.kt
Normal file
@@ -0,0 +1,72 @@
|
||||
package eu.kanade.data
|
||||
|
||||
import androidx.paging.PagingSource
|
||||
import androidx.paging.PagingState
|
||||
import com.squareup.sqldelight.Query
|
||||
import eu.kanade.tachiyomi.Database
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
class QueryPagingSource<RowType : Any>(
|
||||
val handler: DatabaseHandler,
|
||||
val countQuery: Database.() -> Query<Long>,
|
||||
val queryProvider: Database.(Long, Long) -> Query<RowType>,
|
||||
) : PagingSource<Long, RowType>(), Query.Listener {
|
||||
|
||||
override val jumpingSupported: Boolean = true
|
||||
|
||||
private var currentQuery: Query<RowType>? by Delegates.observable(null) { _, old, new ->
|
||||
old?.removeListener(this)
|
||||
new?.addListener(this)
|
||||
}
|
||||
|
||||
init {
|
||||
registerInvalidatedCallback {
|
||||
currentQuery?.removeListener(this)
|
||||
currentQuery = null
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun load(params: LoadParams<Long>): LoadResult<Long, RowType> {
|
||||
try {
|
||||
val key = params.key ?: 0L
|
||||
val loadSize = params.loadSize
|
||||
val count = handler.awaitOne { countQuery() }
|
||||
|
||||
val (offset, limit) = when (params) {
|
||||
is LoadParams.Prepend -> key - loadSize to loadSize.toLong()
|
||||
else -> key to loadSize.toLong()
|
||||
}
|
||||
|
||||
val data = handler.awaitList {
|
||||
queryProvider(limit, offset)
|
||||
.also { currentQuery = it }
|
||||
}
|
||||
|
||||
val (prevKey, nextKey) = when (params) {
|
||||
is LoadParams.Append -> { offset - loadSize to offset + loadSize }
|
||||
else -> { offset to offset + loadSize }
|
||||
}
|
||||
|
||||
return LoadResult.Page(
|
||||
data = data,
|
||||
prevKey = if (offset <= 0L || prevKey < 0L) null else prevKey,
|
||||
nextKey = if (offset + loadSize >= count) null else nextKey,
|
||||
itemsBefore = maxOf(0L, offset).toInt(),
|
||||
itemsAfter = maxOf(0L, count - (offset + loadSize)).toInt(),
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
return LoadResult.Error(throwable = e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getRefreshKey(state: PagingState<Long, RowType>): Long? {
|
||||
return state.anchorPosition?.let { anchorPosition ->
|
||||
val anchorPage = state.closestPageToPosition(anchorPosition)
|
||||
anchorPage?.prevKey ?: anchorPage?.nextKey
|
||||
}
|
||||
}
|
||||
|
||||
override fun queryResultsChanged() {
|
||||
invalidate()
|
||||
}
|
||||
}
|
161
app/src/main/java/eu/kanade/data/TransactionContext.kt
Normal file
161
app/src/main/java/eu/kanade/data/TransactionContext.kt
Normal file
@@ -0,0 +1,161 @@
|
||||
package eu.kanade.data
|
||||
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.asContextElement
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.concurrent.RejectedExecutionException
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import kotlin.coroutines.ContinuationInterceptor
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.coroutines.EmptyCoroutineContext
|
||||
import kotlin.coroutines.coroutineContext
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
/**
|
||||
* Returns the transaction dispatcher if we are on a transaction, or the database dispatchers.
|
||||
*/
|
||||
internal suspend fun AndroidDatabaseHandler.getCurrentDatabaseContext(): CoroutineContext {
|
||||
return coroutineContext[TransactionElement]?.transactionDispatcher ?: queryDispatcher
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the specified suspending [block] in a database transaction. The transaction will be
|
||||
* marked as successful unless an exception is thrown in the suspending [block] or the coroutine
|
||||
* is cancelled.
|
||||
*
|
||||
* SQLDelight will only perform at most one transaction at a time, additional transactions are queued
|
||||
* and executed on a first come, first serve order.
|
||||
*
|
||||
* Performing blocking database operations is not permitted in a coroutine scope other than the
|
||||
* one received by the suspending block. It is recommended that all [Dao] function invoked within
|
||||
* the [block] be suspending functions.
|
||||
*
|
||||
* The dispatcher used to execute the given [block] will utilize threads from SQLDelight's query executor.
|
||||
*/
|
||||
internal suspend fun <T> AndroidDatabaseHandler.withTransaction(block: suspend () -> T): T {
|
||||
// Use inherited transaction context if available, this allows nested suspending transactions.
|
||||
val transactionContext =
|
||||
coroutineContext[TransactionElement]?.transactionDispatcher ?: createTransactionContext()
|
||||
return withContext(transactionContext) {
|
||||
val transactionElement = coroutineContext[TransactionElement]!!
|
||||
transactionElement.acquire()
|
||||
try {
|
||||
db.transactionWithResult {
|
||||
runBlocking(transactionContext) {
|
||||
block()
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
transactionElement.release()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a [CoroutineContext] for performing database operations within a coroutine transaction.
|
||||
*
|
||||
* The context is a combination of a dispatcher, a [TransactionElement] and a thread local element.
|
||||
*
|
||||
* * The dispatcher will dispatch coroutines to a single thread that is taken over from the SQLDelight
|
||||
* query executor. If the coroutine context is switched, suspending DAO functions will be able to
|
||||
* dispatch to the transaction thread.
|
||||
*
|
||||
* * The [TransactionElement] serves as an indicator for inherited context, meaning, if there is a
|
||||
* switch of context, suspending DAO methods will be able to use the indicator to dispatch the
|
||||
* database operation to the transaction thread.
|
||||
*
|
||||
* * The thread local element serves as a second indicator and marks threads that are used to
|
||||
* execute coroutines within the coroutine transaction, more specifically it allows us to identify
|
||||
* if a blocking DAO method is invoked within the transaction coroutine. Never assign meaning to
|
||||
* this value, for now all we care is if its present or not.
|
||||
*/
|
||||
private suspend fun AndroidDatabaseHandler.createTransactionContext(): CoroutineContext {
|
||||
val controlJob = Job()
|
||||
// make sure to tie the control job to this context to avoid blocking the transaction if
|
||||
// context get cancelled before we can even start using this job. Otherwise, the acquired
|
||||
// transaction thread will forever wait for the controlJob to be cancelled.
|
||||
// see b/148181325
|
||||
coroutineContext[Job]?.invokeOnCompletion {
|
||||
controlJob.cancel()
|
||||
}
|
||||
|
||||
val dispatcher = transactionDispatcher.acquireTransactionThread(controlJob)
|
||||
val transactionElement = TransactionElement(controlJob, dispatcher)
|
||||
val threadLocalElement =
|
||||
suspendingTransactionId.asContextElement(System.identityHashCode(controlJob))
|
||||
return dispatcher + transactionElement + threadLocalElement
|
||||
}
|
||||
|
||||
/**
|
||||
* Acquires a thread from the executor and returns a [ContinuationInterceptor] to dispatch
|
||||
* coroutines to the acquired thread. The [controlJob] is used to control the release of the
|
||||
* thread by cancelling the job.
|
||||
*/
|
||||
private suspend fun CoroutineDispatcher.acquireTransactionThread(
|
||||
controlJob: Job,
|
||||
): ContinuationInterceptor {
|
||||
return suspendCancellableCoroutine { continuation ->
|
||||
continuation.invokeOnCancellation {
|
||||
// We got cancelled while waiting to acquire a thread, we can't stop our attempt to
|
||||
// acquire a thread, but we can cancel the controlling job so once it gets acquired it
|
||||
// is quickly released.
|
||||
controlJob.cancel()
|
||||
}
|
||||
try {
|
||||
dispatch(EmptyCoroutineContext) {
|
||||
runBlocking {
|
||||
// Thread acquired, resume coroutine.
|
||||
continuation.resume(coroutineContext[ContinuationInterceptor]!!)
|
||||
controlJob.join()
|
||||
}
|
||||
}
|
||||
} catch (ex: RejectedExecutionException) {
|
||||
// Couldn't acquire a thread, cancel coroutine.
|
||||
continuation.cancel(
|
||||
IllegalStateException(
|
||||
"Unable to acquire a thread to perform the database transaction.",
|
||||
ex,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A [CoroutineContext.Element] that indicates there is an on-going database transaction.
|
||||
*/
|
||||
private class TransactionElement(
|
||||
private val transactionThreadControlJob: Job,
|
||||
val transactionDispatcher: ContinuationInterceptor,
|
||||
) : CoroutineContext.Element {
|
||||
|
||||
companion object Key : CoroutineContext.Key<TransactionElement>
|
||||
|
||||
override val key: CoroutineContext.Key<TransactionElement>
|
||||
get() = TransactionElement
|
||||
|
||||
/**
|
||||
* Number of transactions (including nested ones) started with this element.
|
||||
* Call [acquire] to increase the count and [release] to decrease it. If the count reaches zero
|
||||
* when [release] is invoked then the transaction job is cancelled and the transaction thread
|
||||
* is released.
|
||||
*/
|
||||
private val referenceCount = AtomicInteger(0)
|
||||
|
||||
fun acquire() {
|
||||
referenceCount.incrementAndGet()
|
||||
}
|
||||
|
||||
fun release() {
|
||||
val count = referenceCount.decrementAndGet()
|
||||
if (count < 0) {
|
||||
throw IllegalStateException("Transaction was never started or was already released.")
|
||||
} else if (count == 0) {
|
||||
// Cancel the job that controls the transaction thread, causing it to be released.
|
||||
transactionThreadControlJob.cancel()
|
||||
}
|
||||
}
|
||||
}
|
12
app/src/main/java/eu/kanade/data/category/CategoryMapper.kt
Normal file
12
app/src/main/java/eu/kanade/data/category/CategoryMapper.kt
Normal file
@@ -0,0 +1,12 @@
|
||||
package eu.kanade.data.category
|
||||
|
||||
import eu.kanade.domain.category.model.Category
|
||||
|
||||
val categoryMapper: (Long, String, Long, Long) -> Category = { id, name, order, flags ->
|
||||
Category(
|
||||
id = id,
|
||||
name = name,
|
||||
order = order,
|
||||
flags = flags,
|
||||
)
|
||||
}
|
@@ -0,0 +1,84 @@
|
||||
package eu.kanade.data.category
|
||||
|
||||
import eu.kanade.data.DatabaseHandler
|
||||
import eu.kanade.domain.category.model.Category
|
||||
import eu.kanade.domain.category.model.CategoryUpdate
|
||||
import eu.kanade.domain.category.repository.CategoryRepository
|
||||
import eu.kanade.tachiyomi.Database
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
class CategoryRepositoryImpl(
|
||||
private val handler: DatabaseHandler,
|
||||
) : CategoryRepository {
|
||||
|
||||
override suspend fun get(id: Long): Category? {
|
||||
return handler.awaitOneOrNull { categoriesQueries.getCategory(id, categoryMapper) }
|
||||
}
|
||||
|
||||
override suspend fun getAll(): List<Category> {
|
||||
return handler.awaitList { categoriesQueries.getCategories(categoryMapper) }
|
||||
}
|
||||
|
||||
override fun getAllAsFlow(): Flow<List<Category>> {
|
||||
return handler.subscribeToList { categoriesQueries.getCategories(categoryMapper) }
|
||||
}
|
||||
|
||||
override suspend fun getCategoriesByMangaId(mangaId: Long): List<Category> {
|
||||
return handler.awaitList {
|
||||
categoriesQueries.getCategoriesByMangaId(mangaId, categoryMapper)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getCategoriesByMangaIdAsFlow(mangaId: Long): Flow<List<Category>> {
|
||||
return handler.subscribeToList {
|
||||
categoriesQueries.getCategoriesByMangaId(mangaId, categoryMapper)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun insert(category: Category) {
|
||||
handler.await {
|
||||
categoriesQueries.insert(
|
||||
name = category.name,
|
||||
order = category.order,
|
||||
flags = category.flags,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun updatePartial(update: CategoryUpdate) {
|
||||
handler.await {
|
||||
updatePartialBlocking(update)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun updatePartial(updates: List<CategoryUpdate>) {
|
||||
handler.await(inTransaction = true) {
|
||||
for (update in updates) {
|
||||
updatePartialBlocking(update)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Database.updatePartialBlocking(update: CategoryUpdate) {
|
||||
categoriesQueries.update(
|
||||
name = update.name,
|
||||
order = update.order,
|
||||
flags = update.flags,
|
||||
categoryId = update.id,
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun updateAllFlags(flags: Long?) {
|
||||
handler.await {
|
||||
categoriesQueries.updateAllFlags(flags)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun delete(categoryId: Long) {
|
||||
handler.await {
|
||||
categoriesQueries.delete(
|
||||
categoryId = categoryId,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
21
app/src/main/java/eu/kanade/data/chapter/ChapterMapper.kt
Normal file
21
app/src/main/java/eu/kanade/data/chapter/ChapterMapper.kt
Normal file
@@ -0,0 +1,21 @@
|
||||
package eu.kanade.data.chapter
|
||||
|
||||
import eu.kanade.domain.chapter.model.Chapter
|
||||
|
||||
val chapterMapper: (Long, Long, String, String, String?, Boolean, Boolean, Long, Float, Long, Long, Long) -> Chapter =
|
||||
{ id, mangaId, url, name, scanlator, read, bookmark, lastPageRead, chapterNumber, sourceOrder, dateFetch, dateUpload ->
|
||||
Chapter(
|
||||
id = id,
|
||||
mangaId = mangaId,
|
||||
read = read,
|
||||
bookmark = bookmark,
|
||||
lastPageRead = lastPageRead,
|
||||
dateFetch = dateFetch,
|
||||
sourceOrder = sourceOrder,
|
||||
url = url,
|
||||
name = name,
|
||||
dateUpload = dateUpload,
|
||||
chapterNumber = chapterNumber,
|
||||
scanlator = scanlator,
|
||||
)
|
||||
}
|
@@ -0,0 +1,99 @@
|
||||
package eu.kanade.data.chapter
|
||||
|
||||
import eu.kanade.data.DatabaseHandler
|
||||
import eu.kanade.domain.chapter.model.Chapter
|
||||
import eu.kanade.domain.chapter.model.ChapterUpdate
|
||||
import eu.kanade.domain.chapter.repository.ChapterRepository
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import eu.kanade.tachiyomi.util.system.toLong
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import logcat.LogPriority
|
||||
|
||||
class ChapterRepositoryImpl(
|
||||
private val handler: DatabaseHandler,
|
||||
) : ChapterRepository {
|
||||
|
||||
override suspend fun addAll(chapters: List<Chapter>): List<Chapter> {
|
||||
return try {
|
||||
handler.await(inTransaction = true) {
|
||||
chapters.map { chapter ->
|
||||
chaptersQueries.insert(
|
||||
chapter.mangaId,
|
||||
chapter.url,
|
||||
chapter.name,
|
||||
chapter.scanlator,
|
||||
chapter.read,
|
||||
chapter.bookmark,
|
||||
chapter.lastPageRead,
|
||||
chapter.chapterNumber,
|
||||
chapter.sourceOrder,
|
||||
chapter.dateFetch,
|
||||
chapter.dateUpload,
|
||||
)
|
||||
val lastInsertId = chaptersQueries.selectLastInsertedRowId().executeAsOne()
|
||||
chapter.copy(id = lastInsertId)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR, e)
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun update(chapterUpdate: ChapterUpdate) {
|
||||
partialUpdate(chapterUpdate)
|
||||
}
|
||||
|
||||
override suspend fun updateAll(chapterUpdates: List<ChapterUpdate>) {
|
||||
partialUpdate(*chapterUpdates.toTypedArray())
|
||||
}
|
||||
|
||||
private suspend fun partialUpdate(vararg chapterUpdates: ChapterUpdate) {
|
||||
handler.await(inTransaction = true) {
|
||||
chapterUpdates.forEach { chapterUpdate ->
|
||||
chaptersQueries.update(
|
||||
mangaId = chapterUpdate.mangaId,
|
||||
url = chapterUpdate.url,
|
||||
name = chapterUpdate.name,
|
||||
scanlator = chapterUpdate.scanlator,
|
||||
read = chapterUpdate.read?.toLong(),
|
||||
bookmark = chapterUpdate.bookmark?.toLong(),
|
||||
lastPageRead = chapterUpdate.lastPageRead,
|
||||
chapterNumber = chapterUpdate.chapterNumber?.toDouble(),
|
||||
sourceOrder = chapterUpdate.sourceOrder,
|
||||
dateFetch = chapterUpdate.dateFetch,
|
||||
dateUpload = chapterUpdate.dateUpload,
|
||||
chapterId = chapterUpdate.id,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun removeChaptersWithIds(chapterIds: List<Long>) {
|
||||
try {
|
||||
handler.await { chaptersQueries.removeChaptersWithIds(chapterIds) }
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR, e)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getChapterByMangaId(mangaId: Long): List<Chapter> {
|
||||
return handler.awaitList { chaptersQueries.getChaptersByMangaId(mangaId, chapterMapper) }
|
||||
}
|
||||
|
||||
override suspend fun getBookmarkedChaptersByMangaId(mangaId: Long): List<Chapter> {
|
||||
return handler.awaitList { chaptersQueries.getBookmarkedChaptersByMangaId(mangaId, chapterMapper) }
|
||||
}
|
||||
|
||||
override suspend fun getChapterById(id: Long): Chapter? {
|
||||
return handler.awaitOneOrNull { chaptersQueries.getChapterById(id, chapterMapper) }
|
||||
}
|
||||
|
||||
override suspend fun getChapterByMangaIdAsFlow(mangaId: Long): Flow<List<Chapter>> {
|
||||
return handler.subscribeToList { chaptersQueries.getChaptersByMangaId(mangaId, chapterMapper) }
|
||||
}
|
||||
|
||||
override suspend fun getChapterByUrlAndMangaId(url: String, mangaId: Long): Chapter? {
|
||||
return handler.awaitOneOrNull { chaptersQueries.getChapterByUrlAndMangaId(url, mangaId, chapterMapper) }
|
||||
}
|
||||
}
|
@@ -0,0 +1,47 @@
|
||||
package eu.kanade.data.chapter
|
||||
|
||||
object CleanupChapterName {
|
||||
|
||||
fun await(chapterName: String, mangaTitle: String): String {
|
||||
return chapterName
|
||||
.trim()
|
||||
.removePrefix(mangaTitle)
|
||||
.trim(*CHAPTER_TRIM_CHARS)
|
||||
}
|
||||
|
||||
private val CHAPTER_TRIM_CHARS = arrayOf(
|
||||
// Whitespace
|
||||
' ',
|
||||
'\u0009',
|
||||
'\u000A',
|
||||
'\u000B',
|
||||
'\u000C',
|
||||
'\u000D',
|
||||
'\u0020',
|
||||
'\u0085',
|
||||
'\u00A0',
|
||||
'\u1680',
|
||||
'\u2000',
|
||||
'\u2001',
|
||||
'\u2002',
|
||||
'\u2003',
|
||||
'\u2004',
|
||||
'\u2005',
|
||||
'\u2006',
|
||||
'\u2007',
|
||||
'\u2008',
|
||||
'\u2009',
|
||||
'\u200A',
|
||||
'\u2028',
|
||||
'\u2029',
|
||||
'\u202F',
|
||||
'\u205F',
|
||||
'\u3000',
|
||||
|
||||
// Separators
|
||||
'-',
|
||||
'_',
|
||||
',',
|
||||
':',
|
||||
).toCharArray()
|
||||
}
|
@@ -0,0 +1,3 @@
|
||||
package eu.kanade.data.chapter
|
||||
|
||||
class NoChaptersException : Exception()
|
35
app/src/main/java/eu/kanade/data/history/HistoryMapper.kt
Normal file
35
app/src/main/java/eu/kanade/data/history/HistoryMapper.kt
Normal file
@@ -0,0 +1,35 @@
|
||||
package eu.kanade.data.history
|
||||
|
||||
import eu.kanade.domain.history.model.History
|
||||
import eu.kanade.domain.history.model.HistoryWithRelations
|
||||
import eu.kanade.domain.manga.model.MangaCover
|
||||
import java.util.Date
|
||||
|
||||
val historyMapper: (Long, Long, Date?, Long) -> History = { id, chapterId, readAt, readDuration ->
|
||||
History(
|
||||
id = id,
|
||||
chapterId = chapterId,
|
||||
readAt = readAt,
|
||||
readDuration = readDuration,
|
||||
)
|
||||
}
|
||||
|
||||
val historyWithRelationsMapper: (Long, Long, Long, String, String?, Long, Boolean, Long, Float, Date?, Long) -> HistoryWithRelations = {
|
||||
historyId, mangaId, chapterId, title, thumbnailUrl, sourceId, isFavorite, coverLastModified, chapterNumber, readAt, readDuration ->
|
||||
HistoryWithRelations(
|
||||
id = historyId,
|
||||
chapterId = chapterId,
|
||||
mangaId = mangaId,
|
||||
title = title,
|
||||
chapterNumber = chapterNumber,
|
||||
readAt = readAt,
|
||||
readDuration = readDuration,
|
||||
coverData = MangaCover(
|
||||
mangaId = mangaId,
|
||||
sourceId = sourceId,
|
||||
isMangaFavorite = isFavorite,
|
||||
url = thumbnailUrl,
|
||||
lastModified = coverLastModified,
|
||||
),
|
||||
)
|
||||
}
|
@@ -0,0 +1,66 @@
|
||||
package eu.kanade.data.history
|
||||
|
||||
import eu.kanade.data.DatabaseHandler
|
||||
import eu.kanade.domain.history.model.HistoryUpdate
|
||||
import eu.kanade.domain.history.model.HistoryWithRelations
|
||||
import eu.kanade.domain.history.repository.HistoryRepository
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import logcat.LogPriority
|
||||
|
||||
class HistoryRepositoryImpl(
|
||||
private val handler: DatabaseHandler,
|
||||
) : HistoryRepository {
|
||||
|
||||
override fun getHistory(query: String): Flow<List<HistoryWithRelations>> {
|
||||
return handler.subscribeToList {
|
||||
historyViewQueries.history(query, historyWithRelationsMapper)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getLastHistory(): HistoryWithRelations? {
|
||||
return handler.awaitOneOrNull {
|
||||
historyViewQueries.getLatestHistory(historyWithRelationsMapper)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun resetHistory(historyId: Long) {
|
||||
try {
|
||||
handler.await { historyQueries.resetHistoryById(historyId) }
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR, throwable = e)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun resetHistoryByMangaId(mangaId: Long) {
|
||||
try {
|
||||
handler.await { historyQueries.resetHistoryByMangaId(mangaId) }
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR, throwable = e)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun deleteAllHistory(): Boolean {
|
||||
return try {
|
||||
handler.await { historyQueries.removeAllHistory() }
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR, throwable = e)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun upsertHistory(historyUpdate: HistoryUpdate) {
|
||||
try {
|
||||
handler.await {
|
||||
historyQueries.upsert(
|
||||
historyUpdate.chapterId,
|
||||
historyUpdate.readAt,
|
||||
historyUpdate.sessionReadDuration,
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR, throwable = e)
|
||||
}
|
||||
}
|
||||
}
|
63
app/src/main/java/eu/kanade/data/manga/MangaMapper.kt
Normal file
63
app/src/main/java/eu/kanade/data/manga/MangaMapper.kt
Normal file
@@ -0,0 +1,63 @@
|
||||
package eu.kanade.data.manga
|
||||
|
||||
import eu.kanade.domain.library.model.LibraryManga
|
||||
import eu.kanade.domain.manga.model.Manga
|
||||
import eu.kanade.tachiyomi.source.model.UpdateStrategy
|
||||
|
||||
val mangaMapper: (Long, Long, String, String?, String?, String?, List<String>?, String, Long, String?, Boolean, Long?, Long?, Boolean, Long, Long, Long, Long, UpdateStrategy) -> Manga =
|
||||
{ id, source, url, artist, author, description, genre, title, status, thumbnailUrl, favorite, lastUpdate, _, initialized, viewerFlags, chapterFlags, coverLastModified, dateAdded, updateStrategy ->
|
||||
Manga(
|
||||
id = id,
|
||||
source = source,
|
||||
favorite = favorite,
|
||||
lastUpdate = lastUpdate ?: 0,
|
||||
dateAdded = dateAdded,
|
||||
viewerFlags = viewerFlags,
|
||||
chapterFlags = chapterFlags,
|
||||
coverLastModified = coverLastModified,
|
||||
url = url,
|
||||
title = title,
|
||||
artist = artist,
|
||||
author = author,
|
||||
description = description,
|
||||
genre = genre,
|
||||
status = status,
|
||||
thumbnailUrl = thumbnailUrl,
|
||||
updateStrategy = updateStrategy,
|
||||
initialized = initialized,
|
||||
)
|
||||
}
|
||||
|
||||
val libraryManga: (Long, Long, String, String?, String?, String?, List<String>?, String, Long, String?, Boolean, Long?, Long?, Boolean, Long, Long, Long, Long, UpdateStrategy, Long, Long, Long, Long, Long, Long, Long) -> LibraryManga =
|
||||
{ id, source, url, artist, author, description, genre, title, status, thumbnailUrl, favorite, lastUpdate, nextUpdate, initialized, viewerFlags, chapterFlags, coverLastModified, dateAdded, updateStrategy, totalCount, readCount, latestUpload, chapterFetchedAt, lastRead, bookmarkCount, category ->
|
||||
LibraryManga(
|
||||
manga = mangaMapper(
|
||||
id,
|
||||
source,
|
||||
url,
|
||||
artist,
|
||||
author,
|
||||
description,
|
||||
genre,
|
||||
title,
|
||||
status,
|
||||
thumbnailUrl,
|
||||
favorite,
|
||||
lastUpdate,
|
||||
nextUpdate,
|
||||
initialized,
|
||||
viewerFlags,
|
||||
chapterFlags,
|
||||
coverLastModified,
|
||||
dateAdded,
|
||||
updateStrategy,
|
||||
),
|
||||
category = category,
|
||||
totalChapters = totalCount,
|
||||
readCount = readCount,
|
||||
bookmarkCount = bookmarkCount,
|
||||
latestUpload = latestUpload,
|
||||
chapterFetchedAt = chapterFetchedAt,
|
||||
lastRead = lastRead,
|
||||
)
|
||||
}
|
148
app/src/main/java/eu/kanade/data/manga/MangaRepositoryImpl.kt
Normal file
148
app/src/main/java/eu/kanade/data/manga/MangaRepositoryImpl.kt
Normal file
@@ -0,0 +1,148 @@
|
||||
package eu.kanade.data.manga
|
||||
|
||||
import eu.kanade.data.DatabaseHandler
|
||||
import eu.kanade.data.listOfStringsAdapter
|
||||
import eu.kanade.data.updateStrategyAdapter
|
||||
import eu.kanade.domain.library.model.LibraryManga
|
||||
import eu.kanade.domain.manga.model.Manga
|
||||
import eu.kanade.domain.manga.model.MangaUpdate
|
||||
import eu.kanade.domain.manga.repository.MangaRepository
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import eu.kanade.tachiyomi.util.system.toLong
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import logcat.LogPriority
|
||||
|
||||
class MangaRepositoryImpl(
|
||||
private val handler: DatabaseHandler,
|
||||
) : MangaRepository {
|
||||
|
||||
override suspend fun getMangaById(id: Long): Manga {
|
||||
return handler.awaitOne { mangasQueries.getMangaById(id, mangaMapper) }
|
||||
}
|
||||
|
||||
override suspend fun getMangaByIdAsFlow(id: Long): Flow<Manga> {
|
||||
return handler.subscribeToOne { mangasQueries.getMangaById(id, mangaMapper) }
|
||||
}
|
||||
|
||||
override suspend fun getMangaByUrlAndSourceId(url: String, sourceId: Long): Manga? {
|
||||
return handler.awaitOneOrNull(inTransaction = true) { mangasQueries.getMangaByUrlAndSource(url, sourceId, mangaMapper) }
|
||||
}
|
||||
|
||||
override fun getMangaByUrlAndSourceIdAsFlow(url: String, sourceId: Long): Flow<Manga?> {
|
||||
return handler.subscribeToOneOrNull { mangasQueries.getMangaByUrlAndSource(url, sourceId, mangaMapper) }
|
||||
}
|
||||
|
||||
override suspend fun getFavorites(): List<Manga> {
|
||||
return handler.awaitList { mangasQueries.getFavorites(mangaMapper) }
|
||||
}
|
||||
|
||||
override suspend fun getLibraryManga(): List<LibraryManga> {
|
||||
return handler.awaitList { libraryViewQueries.library(libraryManga) }
|
||||
}
|
||||
|
||||
override fun getLibraryMangaAsFlow(): Flow<List<LibraryManga>> {
|
||||
return handler.subscribeToList { libraryViewQueries.library(libraryManga) }
|
||||
}
|
||||
|
||||
override fun getFavoritesBySourceId(sourceId: Long): Flow<List<Manga>> {
|
||||
return handler.subscribeToList { mangasQueries.getFavoriteBySourceId(sourceId, mangaMapper) }
|
||||
}
|
||||
|
||||
override suspend fun getDuplicateLibraryManga(title: String, sourceId: Long): Manga? {
|
||||
return handler.awaitOneOrNull {
|
||||
mangasQueries.getDuplicateLibraryManga(title, sourceId, mangaMapper)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun resetViewerFlags(): Boolean {
|
||||
return try {
|
||||
handler.await { mangasQueries.resetViewerFlags() }
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR, e)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun setMangaCategories(mangaId: Long, categoryIds: List<Long>) {
|
||||
handler.await(inTransaction = true) {
|
||||
mangas_categoriesQueries.deleteMangaCategoryByMangaId(mangaId)
|
||||
categoryIds.map { categoryId ->
|
||||
mangas_categoriesQueries.insert(mangaId, categoryId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun insert(manga: Manga): Long? {
|
||||
return handler.awaitOneOrNull(inTransaction = true) {
|
||||
mangasQueries.insert(
|
||||
source = manga.source,
|
||||
url = manga.url,
|
||||
artist = manga.artist,
|
||||
author = manga.author,
|
||||
description = manga.description,
|
||||
genre = manga.genre,
|
||||
title = manga.title,
|
||||
status = manga.status,
|
||||
thumbnailUrl = manga.thumbnailUrl,
|
||||
favorite = manga.favorite,
|
||||
lastUpdate = manga.lastUpdate,
|
||||
nextUpdate = null,
|
||||
initialized = manga.initialized,
|
||||
viewerFlags = manga.viewerFlags,
|
||||
chapterFlags = manga.chapterFlags,
|
||||
coverLastModified = manga.coverLastModified,
|
||||
dateAdded = manga.dateAdded,
|
||||
updateStrategy = manga.updateStrategy,
|
||||
)
|
||||
mangasQueries.selectLastInsertedRowId()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun update(update: MangaUpdate): Boolean {
|
||||
return try {
|
||||
partialUpdate(update)
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR, e)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun updateAll(mangaUpdates: List<MangaUpdate>): Boolean {
|
||||
return try {
|
||||
partialUpdate(*mangaUpdates.toTypedArray())
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR, e)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun partialUpdate(vararg mangaUpdates: MangaUpdate) {
|
||||
handler.await(inTransaction = true) {
|
||||
mangaUpdates.forEach { value ->
|
||||
mangasQueries.update(
|
||||
source = value.source,
|
||||
url = value.url,
|
||||
artist = value.artist,
|
||||
author = value.author,
|
||||
description = value.description,
|
||||
genre = value.genre?.let(listOfStringsAdapter::encode),
|
||||
title = value.title,
|
||||
status = value.status,
|
||||
thumbnailUrl = value.thumbnailUrl,
|
||||
favorite = value.favorite?.toLong(),
|
||||
lastUpdate = value.lastUpdate,
|
||||
initialized = value.initialized?.toLong(),
|
||||
viewer = value.viewerFlags,
|
||||
chapterFlags = value.chapterFlags,
|
||||
coverLastModified = value.coverLastModified,
|
||||
dateAdded = value.dateAdded,
|
||||
mangaId = value.id,
|
||||
updateStrategy = value.updateStrategy?.let(updateStrategyAdapter::encode),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,3 @@
|
||||
package eu.kanade.data.source
|
||||
|
||||
class NoResultsException : Exception()
|
@@ -0,0 +1,23 @@
|
||||
package eu.kanade.data.source
|
||||
|
||||
import eu.kanade.data.DatabaseHandler
|
||||
import eu.kanade.domain.source.model.SourceData
|
||||
import eu.kanade.domain.source.repository.SourceDataRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
class SourceDataRepositoryImpl(
|
||||
private val handler: DatabaseHandler,
|
||||
) : SourceDataRepository {
|
||||
|
||||
override fun subscribeAll(): Flow<List<SourceData>> {
|
||||
return handler.subscribeToList { sourcesQueries.findAll(sourceDataMapper) }
|
||||
}
|
||||
|
||||
override suspend fun getSourceData(id: Long): SourceData? {
|
||||
return handler.awaitOneOrNull { sourcesQueries.findOne(id, sourceDataMapper) }
|
||||
}
|
||||
|
||||
override suspend fun upsertSourceData(id: Long, lang: String, name: String) {
|
||||
handler.await { sourcesQueries.upsert(id, lang, name) }
|
||||
}
|
||||
}
|
24
app/src/main/java/eu/kanade/data/source/SourceMapper.kt
Normal file
24
app/src/main/java/eu/kanade/data/source/SourceMapper.kt
Normal file
@@ -0,0 +1,24 @@
|
||||
package eu.kanade.data.source
|
||||
|
||||
import eu.kanade.domain.source.model.Source
|
||||
import eu.kanade.domain.source.model.SourceData
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
|
||||
val sourceMapper: (eu.kanade.tachiyomi.source.Source) -> Source = { source ->
|
||||
Source(
|
||||
source.id,
|
||||
source.lang,
|
||||
source.name,
|
||||
supportsLatest = false,
|
||||
isStub = source is SourceManager.StubSource,
|
||||
)
|
||||
}
|
||||
|
||||
val catalogueSourceMapper: (CatalogueSource) -> Source = { source ->
|
||||
sourceMapper(source).copy(supportsLatest = source.supportsLatest)
|
||||
}
|
||||
|
||||
val sourceDataMapper: (Long, String, String) -> SourceData = { id, lang, name ->
|
||||
SourceData(id, lang, name)
|
||||
}
|
@@ -0,0 +1,62 @@
|
||||
package eu.kanade.data.source
|
||||
|
||||
import androidx.paging.PagingState
|
||||
import eu.kanade.domain.source.model.SourcePagingSourceType
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.util.lang.awaitSingle
|
||||
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||
|
||||
abstract class SourcePagingSource(
|
||||
protected val source: CatalogueSource,
|
||||
) : SourcePagingSourceType() {
|
||||
|
||||
abstract suspend fun requestNextPage(currentPage: Int): MangasPage
|
||||
|
||||
override suspend fun load(params: LoadParams<Long>): LoadResult<Long, SManga> {
|
||||
val page = params.key ?: 1
|
||||
|
||||
val mangasPage = try {
|
||||
withIOContext {
|
||||
requestNextPage(page.toInt())
|
||||
.takeIf { it.mangas.isNotEmpty() }
|
||||
?: throw NoResultsException()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
return LoadResult.Error(e)
|
||||
}
|
||||
|
||||
return LoadResult.Page(
|
||||
data = mangasPage.mangas,
|
||||
prevKey = null,
|
||||
nextKey = if (mangasPage.hasNextPage) page + 1 else null,
|
||||
)
|
||||
}
|
||||
|
||||
override fun getRefreshKey(state: PagingState<Long, SManga>): Long? {
|
||||
return state.anchorPosition?.let { anchorPosition ->
|
||||
val anchorPage = state.closestPageToPosition(anchorPosition)
|
||||
anchorPage?.prevKey ?: anchorPage?.nextKey
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SourceSearchPagingSource(source: CatalogueSource, val query: String, val filters: FilterList) : SourcePagingSource(source) {
|
||||
override suspend fun requestNextPage(currentPage: Int): MangasPage {
|
||||
return source.fetchSearchManga(currentPage, query, filters).awaitSingle()
|
||||
}
|
||||
}
|
||||
|
||||
class SourcePopularPagingSource(source: CatalogueSource) : SourcePagingSource(source) {
|
||||
override suspend fun requestNextPage(currentPage: Int): MangasPage {
|
||||
return source.fetchPopularManga(currentPage).awaitSingle()
|
||||
}
|
||||
}
|
||||
|
||||
class SourceLatestPagingSource(source: CatalogueSource) : SourcePagingSource(source) {
|
||||
override suspend fun requestNextPage(currentPage: Int): MangasPage {
|
||||
return source.fetchLatestUpdates(currentPage).awaitSingle()
|
||||
}
|
||||
}
|
@@ -0,0 +1,74 @@
|
||||
package eu.kanade.data.source
|
||||
|
||||
import eu.kanade.data.DatabaseHandler
|
||||
import eu.kanade.domain.source.model.Source
|
||||
import eu.kanade.domain.source.model.SourcePagingSourceType
|
||||
import eu.kanade.domain.source.model.SourceWithCount
|
||||
import eu.kanade.domain.source.repository.SourceRepository
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.LocalSource
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
class SourceRepositoryImpl(
|
||||
private val sourceManager: SourceManager,
|
||||
private val handler: DatabaseHandler,
|
||||
) : SourceRepository {
|
||||
|
||||
override fun getSources(): Flow<List<Source>> {
|
||||
return sourceManager.catalogueSources.map { sources ->
|
||||
sources.map(catalogueSourceMapper)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getOnlineSources(): Flow<List<Source>> {
|
||||
return sourceManager.onlineSources.map { sources ->
|
||||
sources.map(sourceMapper)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getSourcesWithFavoriteCount(): Flow<List<Pair<Source, Long>>> {
|
||||
val sourceIdWithFavoriteCount = handler.subscribeToList { mangasQueries.getSourceIdWithFavoriteCount() }
|
||||
return sourceIdWithFavoriteCount.map { sourceIdsWithCount ->
|
||||
sourceIdsWithCount
|
||||
.filterNot { it.source == LocalSource.ID }
|
||||
.map { (sourceId, count) ->
|
||||
val source = sourceManager.getOrStub(sourceId).run {
|
||||
sourceMapper(this)
|
||||
}
|
||||
source to count
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getSourcesWithNonLibraryManga(): Flow<List<SourceWithCount>> {
|
||||
val sourceIdWithNonLibraryManga = handler.subscribeToList { mangasQueries.getSourceIdsWithNonLibraryManga() }
|
||||
return sourceIdWithNonLibraryManga.map { sourceId ->
|
||||
sourceId.map { (sourceId, count) ->
|
||||
val source = sourceManager.getOrStub(sourceId)
|
||||
SourceWithCount(sourceMapper(source), count)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun search(
|
||||
sourceId: Long,
|
||||
query: String,
|
||||
filterList: FilterList,
|
||||
): SourcePagingSourceType {
|
||||
val source = sourceManager.get(sourceId) as CatalogueSource
|
||||
return SourceSearchPagingSource(source, query, filterList)
|
||||
}
|
||||
|
||||
override fun getPopular(sourceId: Long): SourcePagingSourceType {
|
||||
val source = sourceManager.get(sourceId) as CatalogueSource
|
||||
return SourcePopularPagingSource(source)
|
||||
}
|
||||
|
||||
override fun getLatest(sourceId: Long): SourcePagingSourceType {
|
||||
val source = sourceManager.get(sourceId) as CatalogueSource
|
||||
return SourceLatestPagingSource(source)
|
||||
}
|
||||
}
|
22
app/src/main/java/eu/kanade/data/track/TrackMapper.kt
Normal file
22
app/src/main/java/eu/kanade/data/track/TrackMapper.kt
Normal file
@@ -0,0 +1,22 @@
|
||||
package eu.kanade.data.track
|
||||
|
||||
import eu.kanade.domain.track.model.Track
|
||||
|
||||
val trackMapper: (Long, Long, Long, Long, Long?, String, Double, Long, Long, Float, String, Long, Long) -> Track =
|
||||
{ id, mangaId, syncId, remoteId, libraryId, title, lastChapterRead, totalChapters, status, score, remoteUrl, startDate, finishDate ->
|
||||
Track(
|
||||
id = id,
|
||||
mangaId = mangaId,
|
||||
syncId = syncId,
|
||||
remoteId = remoteId,
|
||||
libraryId = libraryId,
|
||||
title = title,
|
||||
lastChapterRead = lastChapterRead,
|
||||
totalChapters = totalChapters,
|
||||
status = status,
|
||||
score = score,
|
||||
remoteUrl = remoteUrl,
|
||||
startDate = startDate,
|
||||
finishDate = finishDate,
|
||||
)
|
||||
}
|
@@ -0,0 +1,67 @@
|
||||
package eu.kanade.data.track
|
||||
|
||||
import eu.kanade.data.DatabaseHandler
|
||||
import eu.kanade.domain.track.model.Track
|
||||
import eu.kanade.domain.track.repository.TrackRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
class TrackRepositoryImpl(
|
||||
private val handler: DatabaseHandler,
|
||||
) : TrackRepository {
|
||||
|
||||
override suspend fun getTracksByMangaId(mangaId: Long): List<Track> {
|
||||
return handler.awaitList {
|
||||
manga_syncQueries.getTracksByMangaId(mangaId, trackMapper)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getTracksAsFlow(): Flow<List<Track>> {
|
||||
return handler.subscribeToList {
|
||||
manga_syncQueries.getTracks(trackMapper)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getTracksByMangaIdAsFlow(mangaId: Long): Flow<List<Track>> {
|
||||
return handler.subscribeToList {
|
||||
manga_syncQueries.getTracksByMangaId(mangaId, trackMapper)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun delete(mangaId: Long, syncId: Long) {
|
||||
handler.await {
|
||||
manga_syncQueries.delete(
|
||||
mangaId = mangaId,
|
||||
syncId = syncId,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun insert(track: Track) {
|
||||
insertValues(track)
|
||||
}
|
||||
|
||||
override suspend fun insertAll(tracks: List<Track>) {
|
||||
insertValues(*tracks.toTypedArray())
|
||||
}
|
||||
|
||||
private suspend fun insertValues(vararg tracks: Track) {
|
||||
handler.await(inTransaction = true) {
|
||||
tracks.forEach { mangaTrack ->
|
||||
manga_syncQueries.insert(
|
||||
mangaId = mangaTrack.mangaId,
|
||||
syncId = mangaTrack.syncId,
|
||||
remoteId = mangaTrack.remoteId,
|
||||
libraryId = mangaTrack.libraryId,
|
||||
title = mangaTrack.title,
|
||||
lastChapterRead = mangaTrack.lastChapterRead,
|
||||
totalChapters = mangaTrack.totalChapters,
|
||||
status = mangaTrack.status,
|
||||
score = mangaTrack.score,
|
||||
remoteUrl = mangaTrack.remoteUrl,
|
||||
startDate = mangaTrack.startDate,
|
||||
finishDate = mangaTrack.finishDate,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
26
app/src/main/java/eu/kanade/data/updates/UpdatesMapper.kt
Normal file
26
app/src/main/java/eu/kanade/data/updates/UpdatesMapper.kt
Normal file
@@ -0,0 +1,26 @@
|
||||
package eu.kanade.data.updates
|
||||
|
||||
import eu.kanade.domain.manga.model.MangaCover
|
||||
import eu.kanade.domain.updates.model.UpdatesWithRelations
|
||||
|
||||
val updateWithRelationMapper: (Long, String, Long, String, String?, Boolean, Boolean, Long, Boolean, String?, Long, Long, Long) -> UpdatesWithRelations = {
|
||||
mangaId, mangaTitle, chapterId, chapterName, scanlator, read, bookmark, sourceId, favorite, thumbnailUrl, coverLastModified, _, dateFetch ->
|
||||
UpdatesWithRelations(
|
||||
mangaId = mangaId,
|
||||
mangaTitle = mangaTitle,
|
||||
chapterId = chapterId,
|
||||
chapterName = chapterName,
|
||||
scanlator = scanlator,
|
||||
read = read,
|
||||
bookmark = bookmark,
|
||||
sourceId = sourceId,
|
||||
dateFetch = dateFetch,
|
||||
coverData = MangaCover(
|
||||
mangaId = mangaId,
|
||||
sourceId = sourceId,
|
||||
isMangaFavorite = favorite,
|
||||
url = thumbnailUrl,
|
||||
lastModified = coverLastModified,
|
||||
),
|
||||
)
|
||||
}
|
@@ -0,0 +1,17 @@
|
||||
package eu.kanade.data.updates
|
||||
|
||||
import eu.kanade.data.DatabaseHandler
|
||||
import eu.kanade.domain.updates.model.UpdatesWithRelations
|
||||
import eu.kanade.domain.updates.repository.UpdatesRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
class UpdatesRepositoryImpl(
|
||||
val databaseHandler: DatabaseHandler,
|
||||
) : UpdatesRepository {
|
||||
|
||||
override fun subscribeAll(after: Long): Flow<List<UpdatesWithRelations>> {
|
||||
return databaseHandler.subscribeToList {
|
||||
updatesViewQueries.updates(after, updateWithRelationMapper)
|
||||
}
|
||||
}
|
||||
}
|
149
app/src/main/java/eu/kanade/domain/DomainModule.kt
Normal file
149
app/src/main/java/eu/kanade/domain/DomainModule.kt
Normal file
@@ -0,0 +1,149 @@
|
||||
package eu.kanade.domain
|
||||
|
||||
import eu.kanade.data.category.CategoryRepositoryImpl
|
||||
import eu.kanade.data.chapter.ChapterRepositoryImpl
|
||||
import eu.kanade.data.history.HistoryRepositoryImpl
|
||||
import eu.kanade.data.manga.MangaRepositoryImpl
|
||||
import eu.kanade.data.source.SourceDataRepositoryImpl
|
||||
import eu.kanade.data.source.SourceRepositoryImpl
|
||||
import eu.kanade.data.track.TrackRepositoryImpl
|
||||
import eu.kanade.data.updates.UpdatesRepositoryImpl
|
||||
import eu.kanade.domain.category.interactor.CreateCategoryWithName
|
||||
import eu.kanade.domain.category.interactor.DeleteCategory
|
||||
import eu.kanade.domain.category.interactor.GetCategories
|
||||
import eu.kanade.domain.category.interactor.RenameCategory
|
||||
import eu.kanade.domain.category.interactor.ReorderCategory
|
||||
import eu.kanade.domain.category.interactor.ResetCategoryFlags
|
||||
import eu.kanade.domain.category.interactor.SetDisplayModeForCategory
|
||||
import eu.kanade.domain.category.interactor.SetMangaCategories
|
||||
import eu.kanade.domain.category.interactor.SetSortModeForCategory
|
||||
import eu.kanade.domain.category.interactor.UpdateCategory
|
||||
import eu.kanade.domain.category.repository.CategoryRepository
|
||||
import eu.kanade.domain.chapter.interactor.GetChapter
|
||||
import eu.kanade.domain.chapter.interactor.GetChapterByMangaId
|
||||
import eu.kanade.domain.chapter.interactor.SetMangaDefaultChapterFlags
|
||||
import eu.kanade.domain.chapter.interactor.SetReadStatus
|
||||
import eu.kanade.domain.chapter.interactor.ShouldUpdateDbChapter
|
||||
import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource
|
||||
import eu.kanade.domain.chapter.interactor.SyncChaptersWithTrackServiceTwoWay
|
||||
import eu.kanade.domain.chapter.interactor.UpdateChapter
|
||||
import eu.kanade.domain.chapter.repository.ChapterRepository
|
||||
import eu.kanade.domain.download.interactor.DeleteDownload
|
||||
import eu.kanade.domain.extension.interactor.GetExtensionLanguages
|
||||
import eu.kanade.domain.extension.interactor.GetExtensionSources
|
||||
import eu.kanade.domain.extension.interactor.GetExtensionsByType
|
||||
import eu.kanade.domain.history.interactor.DeleteAllHistory
|
||||
import eu.kanade.domain.history.interactor.GetHistory
|
||||
import eu.kanade.domain.history.interactor.GetNextUnreadChapters
|
||||
import eu.kanade.domain.history.interactor.RemoveHistoryById
|
||||
import eu.kanade.domain.history.interactor.RemoveHistoryByMangaId
|
||||
import eu.kanade.domain.history.interactor.UpsertHistory
|
||||
import eu.kanade.domain.history.repository.HistoryRepository
|
||||
import eu.kanade.domain.manga.interactor.GetDuplicateLibraryManga
|
||||
import eu.kanade.domain.manga.interactor.GetFavorites
|
||||
import eu.kanade.domain.manga.interactor.GetLibraryManga
|
||||
import eu.kanade.domain.manga.interactor.GetManga
|
||||
import eu.kanade.domain.manga.interactor.GetMangaWithChapters
|
||||
import eu.kanade.domain.manga.interactor.NetworkToLocalManga
|
||||
import eu.kanade.domain.manga.interactor.ResetViewerFlags
|
||||
import eu.kanade.domain.manga.interactor.SetMangaChapterFlags
|
||||
import eu.kanade.domain.manga.interactor.SetMangaViewerFlags
|
||||
import eu.kanade.domain.manga.interactor.UpdateManga
|
||||
import eu.kanade.domain.manga.repository.MangaRepository
|
||||
import eu.kanade.domain.source.interactor.GetEnabledSources
|
||||
import eu.kanade.domain.source.interactor.GetLanguagesWithSources
|
||||
import eu.kanade.domain.source.interactor.GetRemoteManga
|
||||
import eu.kanade.domain.source.interactor.GetSourcesWithFavoriteCount
|
||||
import eu.kanade.domain.source.interactor.GetSourcesWithNonLibraryManga
|
||||
import eu.kanade.domain.source.interactor.SetMigrateSorting
|
||||
import eu.kanade.domain.source.interactor.ToggleLanguage
|
||||
import eu.kanade.domain.source.interactor.ToggleSource
|
||||
import eu.kanade.domain.source.interactor.ToggleSourcePin
|
||||
import eu.kanade.domain.source.repository.SourceDataRepository
|
||||
import eu.kanade.domain.source.repository.SourceRepository
|
||||
import eu.kanade.domain.track.interactor.DeleteTrack
|
||||
import eu.kanade.domain.track.interactor.GetTracks
|
||||
import eu.kanade.domain.track.interactor.GetTracksPerManga
|
||||
import eu.kanade.domain.track.interactor.InsertTrack
|
||||
import eu.kanade.domain.track.repository.TrackRepository
|
||||
import eu.kanade.domain.updates.interactor.GetUpdates
|
||||
import eu.kanade.domain.updates.repository.UpdatesRepository
|
||||
import uy.kohesive.injekt.api.InjektModule
|
||||
import uy.kohesive.injekt.api.InjektRegistrar
|
||||
import uy.kohesive.injekt.api.addFactory
|
||||
import uy.kohesive.injekt.api.addSingletonFactory
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class DomainModule : InjektModule {
|
||||
|
||||
override fun InjektRegistrar.registerInjectables() {
|
||||
addSingletonFactory<CategoryRepository> { CategoryRepositoryImpl(get()) }
|
||||
addFactory { GetCategories(get()) }
|
||||
addFactory { ResetCategoryFlags(get(), get()) }
|
||||
addFactory { SetDisplayModeForCategory(get(), get()) }
|
||||
addFactory { SetSortModeForCategory(get(), get()) }
|
||||
addFactory { CreateCategoryWithName(get(), get()) }
|
||||
addFactory { RenameCategory(get()) }
|
||||
addFactory { ReorderCategory(get()) }
|
||||
addFactory { UpdateCategory(get()) }
|
||||
addFactory { DeleteCategory(get()) }
|
||||
|
||||
addSingletonFactory<MangaRepository> { MangaRepositoryImpl(get()) }
|
||||
addFactory { GetDuplicateLibraryManga(get()) }
|
||||
addFactory { GetFavorites(get()) }
|
||||
addFactory { GetLibraryManga(get()) }
|
||||
addFactory { GetMangaWithChapters(get(), get()) }
|
||||
addFactory { GetManga(get()) }
|
||||
addFactory { GetNextUnreadChapters(get(), get(), get()) }
|
||||
addFactory { ResetViewerFlags(get()) }
|
||||
addFactory { SetMangaChapterFlags(get()) }
|
||||
addFactory { SetMangaDefaultChapterFlags(get(), get(), get()) }
|
||||
addFactory { SetMangaViewerFlags(get()) }
|
||||
addFactory { NetworkToLocalManga(get()) }
|
||||
addFactory { UpdateManga(get()) }
|
||||
addFactory { SetMangaCategories(get()) }
|
||||
|
||||
addSingletonFactory<TrackRepository> { TrackRepositoryImpl(get()) }
|
||||
addFactory { DeleteTrack(get()) }
|
||||
addFactory { GetTracksPerManga(get()) }
|
||||
addFactory { GetTracks(get()) }
|
||||
addFactory { InsertTrack(get()) }
|
||||
|
||||
addSingletonFactory<ChapterRepository> { ChapterRepositoryImpl(get()) }
|
||||
addFactory { GetChapter(get()) }
|
||||
addFactory { GetChapterByMangaId(get()) }
|
||||
addFactory { UpdateChapter(get()) }
|
||||
addFactory { SetReadStatus(get(), get(), get(), get()) }
|
||||
addFactory { ShouldUpdateDbChapter() }
|
||||
addFactory { SyncChaptersWithSource(get(), get(), get(), get()) }
|
||||
addFactory { SyncChaptersWithTrackServiceTwoWay(get(), get()) }
|
||||
|
||||
addSingletonFactory<HistoryRepository> { HistoryRepositoryImpl(get()) }
|
||||
addFactory { DeleteAllHistory(get()) }
|
||||
addFactory { GetHistory(get()) }
|
||||
addFactory { UpsertHistory(get()) }
|
||||
addFactory { RemoveHistoryById(get()) }
|
||||
addFactory { RemoveHistoryByMangaId(get()) }
|
||||
|
||||
addFactory { DeleteDownload(get(), get()) }
|
||||
|
||||
addFactory { GetExtensionsByType(get(), get()) }
|
||||
addFactory { GetExtensionSources(get()) }
|
||||
addFactory { GetExtensionLanguages(get(), get()) }
|
||||
|
||||
addSingletonFactory<UpdatesRepository> { UpdatesRepositoryImpl(get()) }
|
||||
addFactory { GetUpdates(get(), get()) }
|
||||
|
||||
addSingletonFactory<SourceRepository> { SourceRepositoryImpl(get(), get()) }
|
||||
addSingletonFactory<SourceDataRepository> { SourceDataRepositoryImpl(get()) }
|
||||
addFactory { GetEnabledSources(get(), get()) }
|
||||
addFactory { GetLanguagesWithSources(get(), get()) }
|
||||
addFactory { GetRemoteManga(get()) }
|
||||
addFactory { GetSourcesWithFavoriteCount(get(), get()) }
|
||||
addFactory { GetSourcesWithNonLibraryManga(get()) }
|
||||
addFactory { SetMigrateSorting(get()) }
|
||||
addFactory { ToggleLanguage(get()) }
|
||||
addFactory { ToggleSource(get()) }
|
||||
addFactory { ToggleSourcePin(get()) }
|
||||
}
|
||||
}
|
@@ -0,0 +1,16 @@
|
||||
package eu.kanade.domain.backup.service
|
||||
|
||||
import eu.kanade.tachiyomi.core.preference.PreferenceStore
|
||||
import eu.kanade.tachiyomi.core.provider.FolderProvider
|
||||
|
||||
class BackupPreferences(
|
||||
private val folderProvider: FolderProvider,
|
||||
private val preferenceStore: PreferenceStore,
|
||||
) {
|
||||
|
||||
fun backupsDirectory() = preferenceStore.getString("backup_directory", folderProvider.path())
|
||||
|
||||
fun numberOfBackups() = preferenceStore.getInt("backup_slots", 2)
|
||||
|
||||
fun backupInterval() = preferenceStore.getInt("backup_interval", 12)
|
||||
}
|
30
app/src/main/java/eu/kanade/domain/base/BasePreferences.kt
Normal file
30
app/src/main/java/eu/kanade/domain/base/BasePreferences.kt
Normal file
@@ -0,0 +1,30 @@
|
||||
package eu.kanade.domain.base
|
||||
|
||||
import android.content.Context
|
||||
import eu.kanade.tachiyomi.core.preference.PreferenceStore
|
||||
import eu.kanade.tachiyomi.core.preference.getEnum
|
||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues
|
||||
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
||||
import eu.kanade.tachiyomi.util.system.isPreviewBuildType
|
||||
import eu.kanade.tachiyomi.util.system.isReleaseBuildType
|
||||
|
||||
class BasePreferences(
|
||||
val context: Context,
|
||||
private val preferenceStore: PreferenceStore,
|
||||
) {
|
||||
|
||||
fun confirmExit() = preferenceStore.getBoolean("pref_confirm_exit", false)
|
||||
|
||||
fun downloadedOnly() = preferenceStore.getBoolean("pref_downloaded_only", false)
|
||||
|
||||
fun incognitoMode() = preferenceStore.getBoolean("incognito_mode", false)
|
||||
|
||||
fun automaticExtUpdates() = preferenceStore.getBoolean("automatic_ext_updates", true)
|
||||
|
||||
fun extensionInstaller() = preferenceStore.getEnum(
|
||||
"extension_installer",
|
||||
if (DeviceUtil.isMiui) PreferenceValues.ExtensionInstaller.LEGACY else PreferenceValues.ExtensionInstaller.PACKAGEINSTALLER,
|
||||
)
|
||||
|
||||
fun acraEnabled() = preferenceStore.getBoolean("acra.enable", isPreviewBuildType || isReleaseBuildType)
|
||||
}
|
@@ -0,0 +1,52 @@
|
||||
package eu.kanade.domain.category.interactor
|
||||
|
||||
import eu.kanade.domain.category.model.Category
|
||||
import eu.kanade.domain.category.model.anyWithName
|
||||
import eu.kanade.domain.category.repository.CategoryRepository
|
||||
import eu.kanade.domain.library.service.LibraryPreferences
|
||||
import eu.kanade.tachiyomi.util.lang.withNonCancellableContext
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import logcat.LogPriority
|
||||
|
||||
class CreateCategoryWithName(
|
||||
private val categoryRepository: CategoryRepository,
|
||||
private val preferences: LibraryPreferences,
|
||||
) {
|
||||
|
||||
private val initialFlags: Long
|
||||
get() {
|
||||
val sort = preferences.librarySortingMode().get()
|
||||
return preferences.libraryDisplayMode().get().flag or
|
||||
sort.type.flag or
|
||||
sort.direction.flag
|
||||
}
|
||||
|
||||
suspend fun await(name: String): Result = withNonCancellableContext {
|
||||
val categories = categoryRepository.getAll()
|
||||
if (categories.anyWithName(name)) {
|
||||
return@withNonCancellableContext Result.NameAlreadyExistsError
|
||||
}
|
||||
|
||||
val nextOrder = categories.maxOfOrNull { it.order }?.plus(1) ?: 0
|
||||
val newCategory = Category(
|
||||
id = 0,
|
||||
name = name,
|
||||
order = nextOrder,
|
||||
flags = initialFlags,
|
||||
)
|
||||
|
||||
try {
|
||||
categoryRepository.insert(newCategory)
|
||||
Result.Success
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR, e)
|
||||
Result.InternalError(e)
|
||||
}
|
||||
}
|
||||
|
||||
sealed class Result {
|
||||
object Success : Result()
|
||||
object NameAlreadyExistsError : Result()
|
||||
data class InternalError(val error: Throwable) : Result()
|
||||
}
|
||||
}
|
@@ -0,0 +1,42 @@
|
||||
package eu.kanade.domain.category.interactor
|
||||
|
||||
import eu.kanade.domain.category.model.CategoryUpdate
|
||||
import eu.kanade.domain.category.repository.CategoryRepository
|
||||
import eu.kanade.tachiyomi.util.lang.withNonCancellableContext
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import logcat.LogPriority
|
||||
|
||||
class DeleteCategory(
|
||||
private val categoryRepository: CategoryRepository,
|
||||
) {
|
||||
|
||||
suspend fun await(categoryId: Long) = withNonCancellableContext {
|
||||
try {
|
||||
categoryRepository.delete(categoryId)
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR, e)
|
||||
return@withNonCancellableContext Result.InternalError(e)
|
||||
}
|
||||
|
||||
val categories = categoryRepository.getAll()
|
||||
val updates = categories.mapIndexed { index, category ->
|
||||
CategoryUpdate(
|
||||
id = category.id,
|
||||
order = index.toLong(),
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
categoryRepository.updatePartial(updates)
|
||||
Result.Success
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR, e)
|
||||
Result.InternalError(e)
|
||||
}
|
||||
}
|
||||
|
||||
sealed class Result {
|
||||
object Success : Result()
|
||||
data class InternalError(val error: Throwable) : Result()
|
||||
}
|
||||
}
|
@@ -0,0 +1,26 @@
|
||||
package eu.kanade.domain.category.interactor
|
||||
|
||||
import eu.kanade.domain.category.model.Category
|
||||
import eu.kanade.domain.category.repository.CategoryRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
class GetCategories(
|
||||
private val categoryRepository: CategoryRepository,
|
||||
) {
|
||||
|
||||
fun subscribe(): Flow<List<Category>> {
|
||||
return categoryRepository.getAllAsFlow()
|
||||
}
|
||||
|
||||
fun subscribe(mangaId: Long): Flow<List<Category>> {
|
||||
return categoryRepository.getCategoriesByMangaIdAsFlow(mangaId)
|
||||
}
|
||||
|
||||
suspend fun await(): List<Category> {
|
||||
return categoryRepository.getAll()
|
||||
}
|
||||
|
||||
suspend fun await(mangaId: Long): List<Category> {
|
||||
return categoryRepository.getCategoriesByMangaId(mangaId)
|
||||
}
|
||||
}
|
@@ -0,0 +1,42 @@
|
||||
package eu.kanade.domain.category.interactor
|
||||
|
||||
import eu.kanade.domain.category.model.Category
|
||||
import eu.kanade.domain.category.model.CategoryUpdate
|
||||
import eu.kanade.domain.category.model.anyWithName
|
||||
import eu.kanade.domain.category.repository.CategoryRepository
|
||||
import eu.kanade.tachiyomi.util.lang.withNonCancellableContext
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import logcat.LogPriority
|
||||
|
||||
class RenameCategory(
|
||||
private val categoryRepository: CategoryRepository,
|
||||
) {
|
||||
|
||||
suspend fun await(categoryId: Long, name: String) = withNonCancellableContext {
|
||||
val categories = categoryRepository.getAll()
|
||||
if (categories.anyWithName(name)) {
|
||||
return@withNonCancellableContext Result.NameAlreadyExistsError
|
||||
}
|
||||
|
||||
val update = CategoryUpdate(
|
||||
id = categoryId,
|
||||
name = name,
|
||||
)
|
||||
|
||||
try {
|
||||
categoryRepository.updatePartial(update)
|
||||
Result.Success
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR, e)
|
||||
Result.InternalError(e)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun await(category: Category, name: String) = await(category.id, name)
|
||||
|
||||
sealed class Result {
|
||||
object Success : Result()
|
||||
object NameAlreadyExistsError : Result()
|
||||
data class InternalError(val error: Throwable) : Result()
|
||||
}
|
||||
}
|
@@ -0,0 +1,50 @@
|
||||
package eu.kanade.domain.category.interactor
|
||||
|
||||
import eu.kanade.domain.category.model.Category
|
||||
import eu.kanade.domain.category.model.CategoryUpdate
|
||||
import eu.kanade.domain.category.repository.CategoryRepository
|
||||
import eu.kanade.tachiyomi.util.lang.withNonCancellableContext
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import logcat.LogPriority
|
||||
|
||||
class ReorderCategory(
|
||||
private val categoryRepository: CategoryRepository,
|
||||
) {
|
||||
|
||||
suspend fun await(categoryId: Long, newPosition: Int) = withNonCancellableContext {
|
||||
val categories = categoryRepository.getAll().filterNot(Category::isSystemCategory)
|
||||
|
||||
val currentIndex = categories.indexOfFirst { it.id == categoryId }
|
||||
if (currentIndex == newPosition) {
|
||||
return@withNonCancellableContext Result.Unchanged
|
||||
}
|
||||
|
||||
val reorderedCategories = categories.toMutableList()
|
||||
val reorderedCategory = reorderedCategories.removeAt(currentIndex)
|
||||
reorderedCategories.add(newPosition, reorderedCategory)
|
||||
|
||||
val updates = reorderedCategories.mapIndexed { index, category ->
|
||||
CategoryUpdate(
|
||||
id = category.id,
|
||||
order = index.toLong(),
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
categoryRepository.updatePartial(updates)
|
||||
Result.Success
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR, e)
|
||||
Result.InternalError(e)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun await(category: Category, newPosition: Long): Result =
|
||||
await(category.id, newPosition.toInt())
|
||||
|
||||
sealed class Result {
|
||||
object Success : Result()
|
||||
object Unchanged : Result()
|
||||
data class InternalError(val error: Throwable) : Result()
|
||||
}
|
||||
}
|
@@ -0,0 +1,17 @@
|
||||
package eu.kanade.domain.category.interactor
|
||||
|
||||
import eu.kanade.domain.category.repository.CategoryRepository
|
||||
import eu.kanade.domain.library.model.plus
|
||||
import eu.kanade.domain.library.service.LibraryPreferences
|
||||
|
||||
class ResetCategoryFlags(
|
||||
private val preferences: LibraryPreferences,
|
||||
private val categoryRepository: CategoryRepository,
|
||||
) {
|
||||
|
||||
suspend fun await() {
|
||||
val display = preferences.libraryDisplayMode().get()
|
||||
val sort = preferences.librarySortingMode().get()
|
||||
categoryRepository.updateAllFlags(display + sort.type + sort.direction)
|
||||
}
|
||||
}
|
@@ -0,0 +1,34 @@
|
||||
package eu.kanade.domain.category.interactor
|
||||
|
||||
import eu.kanade.domain.category.model.Category
|
||||
import eu.kanade.domain.category.model.CategoryUpdate
|
||||
import eu.kanade.domain.category.repository.CategoryRepository
|
||||
import eu.kanade.domain.library.model.LibraryDisplayMode
|
||||
import eu.kanade.domain.library.model.plus
|
||||
import eu.kanade.domain.library.service.LibraryPreferences
|
||||
|
||||
class SetDisplayModeForCategory(
|
||||
private val preferences: LibraryPreferences,
|
||||
private val categoryRepository: CategoryRepository,
|
||||
) {
|
||||
|
||||
suspend fun await(categoryId: Long, display: LibraryDisplayMode) {
|
||||
val category = categoryRepository.get(categoryId) ?: return
|
||||
val flags = category.flags + display
|
||||
if (preferences.categorizedDisplaySettings().get()) {
|
||||
categoryRepository.updatePartial(
|
||||
CategoryUpdate(
|
||||
id = category.id,
|
||||
flags = flags,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
preferences.libraryDisplayMode().set(display)
|
||||
categoryRepository.updateAllFlags(flags)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun await(category: Category, display: LibraryDisplayMode) {
|
||||
await(category.id, display)
|
||||
}
|
||||
}
|
@@ -0,0 +1,18 @@
|
||||
package eu.kanade.domain.category.interactor
|
||||
|
||||
import eu.kanade.domain.manga.repository.MangaRepository
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import logcat.LogPriority
|
||||
|
||||
class SetMangaCategories(
|
||||
private val mangaRepository: MangaRepository,
|
||||
) {
|
||||
|
||||
suspend fun await(mangaId: Long, categoryIds: List<Long>) {
|
||||
try {
|
||||
mangaRepository.setMangaCategories(mangaId, categoryIds)
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR, e)
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,34 @@
|
||||
package eu.kanade.domain.category.interactor
|
||||
|
||||
import eu.kanade.domain.category.model.Category
|
||||
import eu.kanade.domain.category.model.CategoryUpdate
|
||||
import eu.kanade.domain.category.repository.CategoryRepository
|
||||
import eu.kanade.domain.library.model.LibrarySort
|
||||
import eu.kanade.domain.library.model.plus
|
||||
import eu.kanade.domain.library.service.LibraryPreferences
|
||||
|
||||
class SetSortModeForCategory(
|
||||
private val preferences: LibraryPreferences,
|
||||
private val categoryRepository: CategoryRepository,
|
||||
) {
|
||||
|
||||
suspend fun await(categoryId: Long, type: LibrarySort.Type, direction: LibrarySort.Direction) {
|
||||
val category = categoryRepository.get(categoryId) ?: return
|
||||
val flags = category.flags + type + direction
|
||||
if (preferences.categorizedDisplaySettings().get()) {
|
||||
categoryRepository.updatePartial(
|
||||
CategoryUpdate(
|
||||
id = category.id,
|
||||
flags = flags,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
preferences.librarySortingMode().set(LibrarySort(type, direction))
|
||||
categoryRepository.updateAllFlags(flags)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun await(category: Category, type: LibrarySort.Type, direction: LibrarySort.Direction) {
|
||||
await(category.id, type, direction)
|
||||
}
|
||||
}
|
@@ -0,0 +1,24 @@
|
||||
package eu.kanade.domain.category.interactor
|
||||
|
||||
import eu.kanade.domain.category.model.CategoryUpdate
|
||||
import eu.kanade.domain.category.repository.CategoryRepository
|
||||
import eu.kanade.tachiyomi.util.lang.withNonCancellableContext
|
||||
|
||||
class UpdateCategory(
|
||||
private val categoryRepository: CategoryRepository,
|
||||
) {
|
||||
|
||||
suspend fun await(payload: CategoryUpdate): Result = withNonCancellableContext {
|
||||
try {
|
||||
categoryRepository.updatePartial(payload)
|
||||
Result.Success
|
||||
} catch (e: Exception) {
|
||||
Result.Error(e)
|
||||
}
|
||||
}
|
||||
|
||||
sealed class Result {
|
||||
object Success : Result()
|
||||
data class Error(val error: Exception) : Result()
|
||||
}
|
||||
}
|
@@ -0,0 +1,22 @@
|
||||
package eu.kanade.domain.category.model
|
||||
|
||||
import java.io.Serializable
|
||||
|
||||
data class Category(
|
||||
val id: Long,
|
||||
val name: String,
|
||||
val order: Long,
|
||||
val flags: Long,
|
||||
) : Serializable {
|
||||
|
||||
val isSystemCategory: Boolean = id == UNCATEGORIZED_ID
|
||||
|
||||
companion object {
|
||||
|
||||
const val UNCATEGORIZED_ID = 0L
|
||||
}
|
||||
}
|
||||
|
||||
internal fun List<Category>.anyWithName(name: String): Boolean {
|
||||
return any { name == it.name }
|
||||
}
|
@@ -0,0 +1,8 @@
|
||||
package eu.kanade.domain.category.model
|
||||
|
||||
data class CategoryUpdate(
|
||||
val id: Long,
|
||||
val name: String? = null,
|
||||
val order: Long? = null,
|
||||
val flags: Long? = null,
|
||||
)
|
@@ -0,0 +1,28 @@
|
||||
package eu.kanade.domain.category.repository
|
||||
|
||||
import eu.kanade.domain.category.model.Category
|
||||
import eu.kanade.domain.category.model.CategoryUpdate
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface CategoryRepository {
|
||||
|
||||
suspend fun get(id: Long): Category?
|
||||
|
||||
suspend fun getAll(): List<Category>
|
||||
|
||||
fun getAllAsFlow(): Flow<List<Category>>
|
||||
|
||||
suspend fun getCategoriesByMangaId(mangaId: Long): List<Category>
|
||||
|
||||
fun getCategoriesByMangaIdAsFlow(mangaId: Long): Flow<List<Category>>
|
||||
|
||||
suspend fun insert(category: Category)
|
||||
|
||||
suspend fun updatePartial(update: CategoryUpdate)
|
||||
|
||||
suspend fun updatePartial(updates: List<CategoryUpdate>)
|
||||
|
||||
suspend fun updateAllFlags(flags: Long?)
|
||||
|
||||
suspend fun delete(categoryId: Long)
|
||||
}
|
@@ -0,0 +1,29 @@
|
||||
package eu.kanade.domain.chapter.interactor
|
||||
|
||||
import eu.kanade.domain.chapter.model.Chapter
|
||||
import eu.kanade.domain.chapter.repository.ChapterRepository
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import logcat.LogPriority
|
||||
|
||||
class GetChapter(
|
||||
private val chapterRepository: ChapterRepository,
|
||||
) {
|
||||
|
||||
suspend fun await(id: Long): Chapter? {
|
||||
return try {
|
||||
chapterRepository.getChapterById(id)
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR, e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun await(url: String, mangaId: Long): Chapter? {
|
||||
return try {
|
||||
chapterRepository.getChapterByUrlAndMangaId(url, mangaId)
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR, e)
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,20 @@
|
||||
package eu.kanade.domain.chapter.interactor
|
||||
|
||||
import eu.kanade.domain.chapter.model.Chapter
|
||||
import eu.kanade.domain.chapter.repository.ChapterRepository
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import logcat.LogPriority
|
||||
|
||||
class GetChapterByMangaId(
|
||||
private val chapterRepository: ChapterRepository,
|
||||
) {
|
||||
|
||||
suspend fun await(mangaId: Long): List<Chapter> {
|
||||
return try {
|
||||
chapterRepository.getChapterByMangaId(mangaId)
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR, e)
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,36 @@
|
||||
package eu.kanade.domain.chapter.interactor
|
||||
|
||||
import eu.kanade.domain.library.service.LibraryPreferences
|
||||
import eu.kanade.domain.manga.interactor.GetFavorites
|
||||
import eu.kanade.domain.manga.interactor.SetMangaChapterFlags
|
||||
import eu.kanade.domain.manga.model.Manga
|
||||
import eu.kanade.tachiyomi.util.lang.withNonCancellableContext
|
||||
|
||||
class SetMangaDefaultChapterFlags(
|
||||
private val libraryPreferences: LibraryPreferences,
|
||||
private val setMangaChapterFlags: SetMangaChapterFlags,
|
||||
private val getFavorites: GetFavorites,
|
||||
) {
|
||||
|
||||
suspend fun await(manga: Manga) {
|
||||
withNonCancellableContext {
|
||||
with(libraryPreferences) {
|
||||
setMangaChapterFlags.awaitSetAllFlags(
|
||||
mangaId = manga.id,
|
||||
unreadFilter = filterChapterByRead().get(),
|
||||
downloadedFilter = filterChapterByDownloaded().get(),
|
||||
bookmarkedFilter = filterChapterByBookmarked().get(),
|
||||
sortingMode = sortChapterBySourceOrNumber().get(),
|
||||
sortingDirection = sortChapterByAscendingOrDescending().get(),
|
||||
displayMode = displayChapterByNameOrNumber().get(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun awaitAll() {
|
||||
withNonCancellableContext {
|
||||
getFavorites.await().forEach { await(it) }
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,80 @@
|
||||
package eu.kanade.domain.chapter.interactor
|
||||
|
||||
import eu.kanade.domain.chapter.model.Chapter
|
||||
import eu.kanade.domain.chapter.model.ChapterUpdate
|
||||
import eu.kanade.domain.chapter.repository.ChapterRepository
|
||||
import eu.kanade.domain.download.interactor.DeleteDownload
|
||||
import eu.kanade.domain.download.service.DownloadPreferences
|
||||
import eu.kanade.domain.manga.model.Manga
|
||||
import eu.kanade.domain.manga.repository.MangaRepository
|
||||
import eu.kanade.tachiyomi.util.lang.withNonCancellableContext
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import logcat.LogPriority
|
||||
|
||||
class SetReadStatus(
|
||||
private val downloadPreferences: DownloadPreferences,
|
||||
private val deleteDownload: DeleteDownload,
|
||||
private val mangaRepository: MangaRepository,
|
||||
private val chapterRepository: ChapterRepository,
|
||||
) {
|
||||
|
||||
private val mapper = { chapter: Chapter, read: Boolean ->
|
||||
ChapterUpdate(
|
||||
read = read,
|
||||
lastPageRead = if (!read) 0 else null,
|
||||
id = chapter.id,
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun await(read: Boolean, vararg chapters: Chapter): Result = withNonCancellableContext {
|
||||
val chaptersToUpdate = chapters.filter {
|
||||
when (read) {
|
||||
true -> !it.read
|
||||
false -> it.read || it.lastPageRead > 0
|
||||
}
|
||||
}
|
||||
if (chaptersToUpdate.isEmpty()) {
|
||||
return@withNonCancellableContext Result.NoChapters
|
||||
}
|
||||
|
||||
try {
|
||||
chapterRepository.updateAll(
|
||||
chaptersToUpdate.map { mapper(it, read) },
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR, e)
|
||||
return@withNonCancellableContext Result.InternalError(e)
|
||||
}
|
||||
|
||||
if (read && downloadPreferences.removeAfterMarkedAsRead().get()) {
|
||||
chaptersToUpdate
|
||||
.groupBy { it.mangaId }
|
||||
.forEach { (mangaId, chapters) ->
|
||||
deleteDownload.awaitAll(
|
||||
manga = mangaRepository.getMangaById(mangaId),
|
||||
chapters = chapters.toTypedArray(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Result.Success
|
||||
}
|
||||
|
||||
suspend fun await(mangaId: Long, read: Boolean): Result = withNonCancellableContext {
|
||||
await(
|
||||
read = read,
|
||||
chapters = chapterRepository
|
||||
.getChapterByMangaId(mangaId)
|
||||
.toTypedArray(),
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun await(manga: Manga, read: Boolean) =
|
||||
await(manga.id, read)
|
||||
|
||||
sealed class Result {
|
||||
object Success : Result()
|
||||
object NoChapters : Result()
|
||||
data class InternalError(val error: Throwable) : Result()
|
||||
}
|
||||
}
|
@@ -0,0 +1,13 @@
|
||||
package eu.kanade.domain.chapter.interactor
|
||||
|
||||
import eu.kanade.domain.chapter.model.Chapter
|
||||
|
||||
class ShouldUpdateDbChapter {
|
||||
|
||||
fun await(dbChapter: Chapter, sourceChapter: Chapter): Boolean {
|
||||
return dbChapter.scanlator != sourceChapter.scanlator || dbChapter.name != sourceChapter.name ||
|
||||
dbChapter.dateUpload != sourceChapter.dateUpload ||
|
||||
dbChapter.chapterNumber != sourceChapter.chapterNumber ||
|
||||
dbChapter.sourceOrder != sourceChapter.sourceOrder
|
||||
}
|
||||
}
|
@@ -0,0 +1,195 @@
|
||||
package eu.kanade.domain.chapter.interactor
|
||||
|
||||
import eu.kanade.data.chapter.CleanupChapterName
|
||||
import eu.kanade.data.chapter.NoChaptersException
|
||||
import eu.kanade.domain.chapter.model.Chapter
|
||||
import eu.kanade.domain.chapter.model.toChapterUpdate
|
||||
import eu.kanade.domain.chapter.model.toDbChapter
|
||||
import eu.kanade.domain.chapter.repository.ChapterRepository
|
||||
import eu.kanade.domain.manga.interactor.UpdateManga
|
||||
import eu.kanade.domain.manga.model.Manga
|
||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||
import eu.kanade.tachiyomi.data.download.DownloadProvider
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.isLocal
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.util.chapter.ChapterRecognition
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.lang.Long.max
|
||||
import java.util.Date
|
||||
import java.util.TreeSet
|
||||
|
||||
class SyncChaptersWithSource(
|
||||
private val downloadManager: DownloadManager = Injekt.get(),
|
||||
private val downloadProvider: DownloadProvider = Injekt.get(),
|
||||
private val chapterRepository: ChapterRepository = Injekt.get(),
|
||||
private val shouldUpdateDbChapter: ShouldUpdateDbChapter = Injekt.get(),
|
||||
private val updateManga: UpdateManga = Injekt.get(),
|
||||
private val updateChapter: UpdateChapter = Injekt.get(),
|
||||
private val getChapterByMangaId: GetChapterByMangaId = Injekt.get(),
|
||||
) {
|
||||
|
||||
/**
|
||||
* Method to synchronize db chapters with source ones
|
||||
*
|
||||
* @param rawSourceChapters the chapters from the source.
|
||||
* @param manga the manga the chapters belong to.
|
||||
* @param source the source the manga belongs to.
|
||||
* @return Newly added chapters
|
||||
*/
|
||||
suspend fun await(
|
||||
rawSourceChapters: List<SChapter>,
|
||||
manga: Manga,
|
||||
source: Source,
|
||||
): List<Chapter> {
|
||||
if (rawSourceChapters.isEmpty() && !source.isLocal()) {
|
||||
throw NoChaptersException()
|
||||
}
|
||||
|
||||
val sourceChapters = rawSourceChapters
|
||||
.distinctBy { it.url }
|
||||
.mapIndexed { i, sChapter ->
|
||||
Chapter.create()
|
||||
.copyFromSChapter(sChapter)
|
||||
.copy(name = CleanupChapterName.await(sChapter.name, manga.title))
|
||||
.copy(mangaId = manga.id, sourceOrder = i.toLong())
|
||||
}
|
||||
|
||||
// Chapters from db.
|
||||
val dbChapters = getChapterByMangaId.await(manga.id)
|
||||
|
||||
// Chapters from the source not in db.
|
||||
val toAdd = mutableListOf<Chapter>()
|
||||
|
||||
// Chapters whose metadata have changed.
|
||||
val toChange = mutableListOf<Chapter>()
|
||||
|
||||
// Chapters from the db not in source.
|
||||
val toDelete = dbChapters.filterNot { dbChapter ->
|
||||
sourceChapters.any { sourceChapter ->
|
||||
dbChapter.url == sourceChapter.url
|
||||
}
|
||||
}
|
||||
|
||||
val rightNow = Date().time
|
||||
|
||||
// Used to not set upload date of older chapters
|
||||
// to a higher value than newer chapters
|
||||
var maxSeenUploadDate = 0L
|
||||
|
||||
val sManga = manga.toSManga()
|
||||
for (sourceChapter in sourceChapters) {
|
||||
var chapter = sourceChapter
|
||||
|
||||
// Update metadata from source if necessary.
|
||||
if (source is HttpSource) {
|
||||
val sChapter = chapter.toSChapter()
|
||||
source.prepareNewChapter(sChapter, sManga)
|
||||
chapter = chapter.copyFromSChapter(sChapter)
|
||||
}
|
||||
|
||||
// Recognize chapter number for the chapter.
|
||||
val chapterNumber = ChapterRecognition.parseChapterNumber(manga.title, chapter.name, chapter.chapterNumber)
|
||||
chapter = chapter.copy(chapterNumber = chapterNumber)
|
||||
|
||||
val dbChapter = dbChapters.find { it.url == chapter.url }
|
||||
|
||||
if (dbChapter == null) {
|
||||
val toAddChapter = if (chapter.dateUpload == 0L) {
|
||||
val altDateUpload = if (maxSeenUploadDate == 0L) rightNow else maxSeenUploadDate
|
||||
chapter.copy(dateUpload = altDateUpload)
|
||||
} else {
|
||||
maxSeenUploadDate = max(maxSeenUploadDate, sourceChapter.dateUpload)
|
||||
chapter
|
||||
}
|
||||
toAdd.add(toAddChapter)
|
||||
} else {
|
||||
if (shouldUpdateDbChapter.await(dbChapter, chapter)) {
|
||||
val shouldRenameChapter = downloadProvider.isChapterDirNameChanged(dbChapter, chapter) &&
|
||||
downloadManager.isChapterDownloaded(dbChapter.name, dbChapter.scanlator, manga.title, manga.source)
|
||||
|
||||
if (shouldRenameChapter) {
|
||||
downloadManager.renameChapter(source, manga, dbChapter.toDbChapter(), chapter.toDbChapter())
|
||||
}
|
||||
var toChangeChapter = dbChapter.copy(
|
||||
name = chapter.name,
|
||||
chapterNumber = chapter.chapterNumber,
|
||||
scanlator = chapter.scanlator,
|
||||
sourceOrder = chapter.sourceOrder,
|
||||
)
|
||||
if (chapter.dateUpload != 0L) {
|
||||
toChangeChapter = toChangeChapter.copy(dateUpload = chapter.dateUpload)
|
||||
}
|
||||
toChange.add(toChangeChapter)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return if there's nothing to add, delete or change, avoiding unnecessary db transactions.
|
||||
if (toAdd.isEmpty() && toDelete.isEmpty() && toChange.isEmpty()) {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
val reAdded = mutableListOf<Chapter>()
|
||||
|
||||
val deletedChapterNumbers = TreeSet<Float>()
|
||||
val deletedReadChapterNumbers = TreeSet<Float>()
|
||||
val deletedBookmarkedChapterNumbers = TreeSet<Float>()
|
||||
|
||||
toDelete.forEach { chapter ->
|
||||
if (chapter.read) deletedReadChapterNumbers.add(chapter.chapterNumber)
|
||||
if (chapter.bookmark) deletedBookmarkedChapterNumbers.add(chapter.chapterNumber)
|
||||
deletedChapterNumbers.add(chapter.chapterNumber)
|
||||
}
|
||||
|
||||
val deletedChapterNumberDateFetchMap = toDelete.sortedByDescending { it.dateFetch }
|
||||
.associate { it.chapterNumber to it.dateFetch }
|
||||
|
||||
// Date fetch is set in such a way that the upper ones will have bigger value than the lower ones
|
||||
// Sources MUST return the chapters from most to less recent, which is common.
|
||||
var itemCount = toAdd.size
|
||||
var updatedToAdd = toAdd.map { toAddItem ->
|
||||
var chapter = toAddItem.copy(dateFetch = rightNow + itemCount--)
|
||||
|
||||
if (chapter.isRecognizedNumber.not() || chapter.chapterNumber !in deletedChapterNumbers) return@map chapter
|
||||
|
||||
chapter = chapter.copy(
|
||||
read = chapter.chapterNumber in deletedReadChapterNumbers,
|
||||
bookmark = chapter.chapterNumber in deletedBookmarkedChapterNumbers,
|
||||
)
|
||||
|
||||
// Try to to use the fetch date of the original entry to not pollute 'Updates' tab
|
||||
deletedChapterNumberDateFetchMap[chapter.chapterNumber]?.let {
|
||||
chapter = chapter.copy(dateFetch = it)
|
||||
}
|
||||
|
||||
reAdded.add(chapter)
|
||||
|
||||
chapter
|
||||
}
|
||||
|
||||
if (toDelete.isNotEmpty()) {
|
||||
val toDeleteIds = toDelete.map { it.id }
|
||||
chapterRepository.removeChaptersWithIds(toDeleteIds)
|
||||
}
|
||||
|
||||
if (updatedToAdd.isNotEmpty()) {
|
||||
updatedToAdd = chapterRepository.addAll(updatedToAdd)
|
||||
}
|
||||
|
||||
if (toChange.isNotEmpty()) {
|
||||
val chapterUpdates = toChange.map { it.toChapterUpdate() }
|
||||
updateChapter.awaitAll(chapterUpdates)
|
||||
}
|
||||
|
||||
// Set this manga as updated since chapters were changed
|
||||
// Note that last_update actually represents last time the chapter list changed at all
|
||||
updateManga.awaitUpdateLastUpdate(manga.id)
|
||||
|
||||
val reAddedUrls = reAdded.map { it.url }.toHashSet()
|
||||
|
||||
return updatedToAdd.filterNot { it.url in reAddedUrls }
|
||||
}
|
||||
}
|
41
app/src/main/java/eu/kanade/domain/chapter/interactor/SyncChaptersWithTrackServiceTwoWay.kt
Normal file
41
app/src/main/java/eu/kanade/domain/chapter/interactor/SyncChaptersWithTrackServiceTwoWay.kt
Normal file
@@ -0,0 +1,41 @@
|
||||
package eu.kanade.domain.chapter.interactor
|
||||
|
||||
import eu.kanade.domain.chapter.model.Chapter
|
||||
import eu.kanade.domain.chapter.model.toChapterUpdate
|
||||
import eu.kanade.domain.track.interactor.InsertTrack
|
||||
import eu.kanade.domain.track.model.Track
|
||||
import eu.kanade.domain.track.model.toDbTrack
|
||||
import eu.kanade.tachiyomi.data.track.TrackService
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import logcat.LogPriority
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class SyncChaptersWithTrackServiceTwoWay(
|
||||
private val updateChapter: UpdateChapter = Injekt.get(),
|
||||
private val insertTrack: InsertTrack = Injekt.get(),
|
||||
) {
|
||||
|
||||
suspend fun await(
|
||||
chapters: List<Chapter>,
|
||||
remoteTrack: Track,
|
||||
service: TrackService,
|
||||
) {
|
||||
val sortedChapters = chapters.sortedBy { it.chapterNumber }
|
||||
val chapterUpdates = sortedChapters
|
||||
.filter { chapter -> chapter.chapterNumber <= remoteTrack.lastChapterRead && !chapter.read }
|
||||
.map { it.copy(read = true).toChapterUpdate() }
|
||||
|
||||
// only take into account continuous reading
|
||||
val localLastRead = sortedChapters.takeWhile { it.read }.lastOrNull()?.chapterNumber ?: 0F
|
||||
val updatedTrack = remoteTrack.copy(lastChapterRead = localLastRead.toDouble())
|
||||
|
||||
try {
|
||||
service.update(updatedTrack.toDbTrack())
|
||||
updateChapter.awaitAll(chapterUpdates)
|
||||
insertTrack.await(updatedTrack)
|
||||
} catch (e: Throwable) {
|
||||
logcat(LogPriority.WARN, e)
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,27 @@
|
||||
package eu.kanade.domain.chapter.interactor
|
||||
|
||||
import eu.kanade.domain.chapter.model.ChapterUpdate
|
||||
import eu.kanade.domain.chapter.repository.ChapterRepository
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import logcat.LogPriority
|
||||
|
||||
class UpdateChapter(
|
||||
private val chapterRepository: ChapterRepository,
|
||||
) {
|
||||
|
||||
suspend fun await(chapterUpdate: ChapterUpdate) {
|
||||
try {
|
||||
chapterRepository.update(chapterUpdate)
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR, e)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun awaitAll(chapterUpdates: List<ChapterUpdate>) {
|
||||
try {
|
||||
chapterRepository.updateAll(chapterUpdates)
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR, e)
|
||||
}
|
||||
}
|
||||
}
|
76
app/src/main/java/eu/kanade/domain/chapter/model/Chapter.kt
Normal file
76
app/src/main/java/eu/kanade/domain/chapter/model/Chapter.kt
Normal file
@@ -0,0 +1,76 @@
|
||||
package eu.kanade.domain.chapter.model
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.models.ChapterImpl
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter as DbChapter
|
||||
|
||||
data class Chapter(
|
||||
val id: Long,
|
||||
val mangaId: Long,
|
||||
val read: Boolean,
|
||||
val bookmark: Boolean,
|
||||
val lastPageRead: Long,
|
||||
val dateFetch: Long,
|
||||
val sourceOrder: Long,
|
||||
val url: String,
|
||||
val name: String,
|
||||
val dateUpload: Long,
|
||||
val chapterNumber: Float,
|
||||
val scanlator: String?,
|
||||
) {
|
||||
val isRecognizedNumber: Boolean
|
||||
get() = chapterNumber >= 0f
|
||||
|
||||
fun toSChapter(): SChapter {
|
||||
return SChapter.create().also {
|
||||
it.url = url
|
||||
it.name = name
|
||||
it.date_upload = dateUpload
|
||||
it.chapter_number = chapterNumber
|
||||
it.scanlator = scanlator
|
||||
}
|
||||
}
|
||||
|
||||
fun copyFromSChapter(sChapter: SChapter): Chapter {
|
||||
return this.copy(
|
||||
name = sChapter.name,
|
||||
url = sChapter.url,
|
||||
dateUpload = sChapter.date_upload,
|
||||
chapterNumber = sChapter.chapter_number,
|
||||
scanlator = sChapter.scanlator?.ifBlank { null },
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun create() = Chapter(
|
||||
id = -1,
|
||||
mangaId = -1,
|
||||
read = false,
|
||||
bookmark = false,
|
||||
lastPageRead = 0,
|
||||
dateFetch = 0,
|
||||
sourceOrder = 0,
|
||||
url = "",
|
||||
name = "",
|
||||
dateUpload = -1,
|
||||
chapterNumber = -1f,
|
||||
scanlator = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Remove when all deps are migrated
|
||||
fun Chapter.toDbChapter(): DbChapter = ChapterImpl().also {
|
||||
it.id = id
|
||||
it.manga_id = mangaId
|
||||
it.url = url
|
||||
it.name = name
|
||||
it.scanlator = scanlator
|
||||
it.read = read
|
||||
it.bookmark = bookmark
|
||||
it.last_page_read = lastPageRead.toInt()
|
||||
it.date_fetch = dateFetch
|
||||
it.date_upload = dateUpload
|
||||
it.chapter_number = chapterNumber
|
||||
it.source_order = sourceOrder.toInt()
|
||||
}
|
@@ -0,0 +1,20 @@
|
||||
package eu.kanade.domain.chapter.model
|
||||
|
||||
data class ChapterUpdate(
|
||||
val id: Long,
|
||||
val mangaId: Long? = null,
|
||||
val read: Boolean? = null,
|
||||
val bookmark: Boolean? = null,
|
||||
val lastPageRead: Long? = null,
|
||||
val dateFetch: Long? = null,
|
||||
val sourceOrder: Long? = null,
|
||||
val url: String? = null,
|
||||
val name: String? = null,
|
||||
val dateUpload: Long? = null,
|
||||
val chapterNumber: Float? = null,
|
||||
val scanlator: String? = null,
|
||||
)
|
||||
|
||||
fun Chapter.toChapterUpdate(): ChapterUpdate {
|
||||
return ChapterUpdate(id, mangaId, read, bookmark, lastPageRead, dateFetch, sourceOrder, url, name, dateUpload, chapterNumber, scanlator)
|
||||
}
|
@@ -0,0 +1,26 @@
|
||||
package eu.kanade.domain.chapter.repository
|
||||
|
||||
import eu.kanade.domain.chapter.model.Chapter
|
||||
import eu.kanade.domain.chapter.model.ChapterUpdate
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface ChapterRepository {
|
||||
|
||||
suspend fun addAll(chapters: List<Chapter>): List<Chapter>
|
||||
|
||||
suspend fun update(chapterUpdate: ChapterUpdate)
|
||||
|
||||
suspend fun updateAll(chapterUpdates: List<ChapterUpdate>)
|
||||
|
||||
suspend fun removeChaptersWithIds(chapterIds: List<Long>)
|
||||
|
||||
suspend fun getChapterByMangaId(mangaId: Long): List<Chapter>
|
||||
|
||||
suspend fun getBookmarkedChaptersByMangaId(mangaId: Long): List<Chapter>
|
||||
|
||||
suspend fun getChapterById(id: Long): Chapter?
|
||||
|
||||
suspend fun getChapterByMangaIdAsFlow(mangaId: Long): Flow<List<Chapter>>
|
||||
|
||||
suspend fun getChapterByUrlAndMangaId(url: String, mangaId: Long): Chapter?
|
||||
}
|
@@ -0,0 +1,20 @@
|
||||
package eu.kanade.domain.download.interactor
|
||||
|
||||
import eu.kanade.domain.chapter.model.Chapter
|
||||
import eu.kanade.domain.chapter.model.toDbChapter
|
||||
import eu.kanade.domain.manga.model.Manga
|
||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.util.lang.withNonCancellableContext
|
||||
|
||||
class DeleteDownload(
|
||||
private val sourceManager: SourceManager,
|
||||
private val downloadManager: DownloadManager,
|
||||
) {
|
||||
|
||||
suspend fun awaitAll(manga: Manga, vararg chapters: Chapter) = withNonCancellableContext {
|
||||
sourceManager.get(manga.source)?.let { source ->
|
||||
downloadManager.deleteChapters(chapters.map { it.toDbChapter() }, manga, source)
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,34 @@
|
||||
package eu.kanade.domain.download.service
|
||||
|
||||
import eu.kanade.tachiyomi.core.preference.PreferenceStore
|
||||
import eu.kanade.tachiyomi.core.provider.FolderProvider
|
||||
|
||||
class DownloadPreferences(
|
||||
private val folderProvider: FolderProvider,
|
||||
private val preferenceStore: PreferenceStore,
|
||||
) {
|
||||
|
||||
fun downloadsDirectory() = preferenceStore.getString("download_directory", folderProvider.path())
|
||||
|
||||
fun downloadOnlyOverWifi() = preferenceStore.getBoolean("pref_download_only_over_wifi_key", true)
|
||||
|
||||
fun saveChaptersAsCBZ() = preferenceStore.getBoolean("save_chapter_as_cbz", true)
|
||||
|
||||
fun splitTallImages() = preferenceStore.getBoolean("split_tall_images", false)
|
||||
|
||||
fun autoDownloadWhileReading() = preferenceStore.getInt("auto_download_while_reading", 0)
|
||||
|
||||
fun removeAfterReadSlots() = preferenceStore.getInt("remove_after_read_slots", -1)
|
||||
|
||||
fun removeAfterMarkedAsRead() = preferenceStore.getBoolean("pref_remove_after_marked_as_read_key", false)
|
||||
|
||||
fun removeBookmarkedChapters() = preferenceStore.getBoolean("pref_remove_bookmarked", false)
|
||||
|
||||
fun removeExcludeCategories() = preferenceStore.getStringSet("remove_exclude_categories", emptySet())
|
||||
|
||||
fun downloadNewChapters() = preferenceStore.getBoolean("download_new", false)
|
||||
|
||||
fun downloadNewChapterCategories() = preferenceStore.getStringSet("download_new_categories", emptySet())
|
||||
|
||||
fun downloadNewChapterCategoriesExclude() = preferenceStore.getStringSet("download_new_categories_exclude", emptySet())
|
||||
}
|
@@ -0,0 +1,35 @@
|
||||
package eu.kanade.domain.extension.interactor
|
||||
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
|
||||
class GetExtensionLanguages(
|
||||
private val preferences: SourcePreferences,
|
||||
private val extensionManager: ExtensionManager,
|
||||
) {
|
||||
fun subscribe(): Flow<List<String>> {
|
||||
return combine(
|
||||
preferences.enabledLanguages().changes(),
|
||||
extensionManager.availableExtensionsFlow,
|
||||
) { enabledLanguage, availableExtensions ->
|
||||
availableExtensions
|
||||
.flatMap { ext ->
|
||||
if (ext.sources.isEmpty()) {
|
||||
listOf(ext.lang)
|
||||
} else {
|
||||
ext.sources.map { it.lang }
|
||||
}
|
||||
}
|
||||
.distinct()
|
||||
.sortedWith(
|
||||
compareBy(
|
||||
{ it !in enabledLanguage },
|
||||
{ LocaleHelper.getDisplayName(it) },
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,32 @@
|
||||
package eu.kanade.domain.extension.interactor
|
||||
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import eu.kanade.tachiyomi.extension.model.Extension
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.ui.browse.extension.details.ExtensionSourceItem
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
class GetExtensionSources(
|
||||
private val preferences: SourcePreferences,
|
||||
) {
|
||||
|
||||
fun subscribe(extension: Extension.Installed): Flow<List<ExtensionSourceItem>> {
|
||||
val isMultiSource = extension.sources.size > 1
|
||||
val isMultiLangSingleSource =
|
||||
isMultiSource && extension.sources.map { it.name }.distinct().size == 1
|
||||
|
||||
return preferences.disabledSources().changes().map { disabledSources ->
|
||||
fun Source.isEnabled() = id.toString() !in disabledSources
|
||||
|
||||
extension.sources
|
||||
.map { source ->
|
||||
ExtensionSourceItem(
|
||||
source = source,
|
||||
enabled = source.isEnabled(),
|
||||
labelAsName = isMultiSource && isMultiLangSingleSource.not(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,60 @@
|
||||
package eu.kanade.domain.extension.interactor
|
||||
|
||||
import eu.kanade.domain.extension.model.Extensions
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||
import eu.kanade.tachiyomi.extension.model.Extension
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
|
||||
class GetExtensionsByType(
|
||||
private val preferences: SourcePreferences,
|
||||
private val extensionManager: ExtensionManager,
|
||||
) {
|
||||
|
||||
fun subscribe(): Flow<Extensions> {
|
||||
val showNsfwSources = preferences.showNsfwSource().get()
|
||||
|
||||
return combine(
|
||||
preferences.enabledLanguages().changes(),
|
||||
extensionManager.installedExtensionsFlow,
|
||||
extensionManager.untrustedExtensionsFlow,
|
||||
extensionManager.availableExtensionsFlow,
|
||||
) { _activeLanguages, _installed, _untrusted, _available ->
|
||||
val (updates, installed) = _installed
|
||||
.filter { (showNsfwSources || it.isNsfw.not()) }
|
||||
.sortedWith(
|
||||
compareBy<Extension.Installed> { it.isObsolete.not() }
|
||||
.thenBy(String.CASE_INSENSITIVE_ORDER) { it.name },
|
||||
)
|
||||
.partition { it.hasUpdate }
|
||||
|
||||
val untrusted = _untrusted
|
||||
.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name })
|
||||
|
||||
val available = _available
|
||||
.filter { extension ->
|
||||
_installed.none { it.pkgName == extension.pkgName } &&
|
||||
_untrusted.none { it.pkgName == extension.pkgName } &&
|
||||
(showNsfwSources || extension.isNsfw.not())
|
||||
}
|
||||
.flatMap { ext ->
|
||||
if (ext.sources.isEmpty()) {
|
||||
return@flatMap if (ext.lang in _activeLanguages) listOf(ext) else emptyList()
|
||||
}
|
||||
ext.sources.filter { it.lang in _activeLanguages }
|
||||
.map {
|
||||
ext.copy(
|
||||
name = it.name,
|
||||
lang = it.lang,
|
||||
pkgName = "${ext.pkgName}-${it.id}",
|
||||
sources = listOf(it),
|
||||
)
|
||||
}
|
||||
}
|
||||
.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name })
|
||||
|
||||
Extensions(updates, installed, available, untrusted)
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,10 @@
|
||||
package eu.kanade.domain.extension.model
|
||||
|
||||
import eu.kanade.tachiyomi.extension.model.Extension
|
||||
|
||||
data class Extensions(
|
||||
val updates: List<Extension.Installed>,
|
||||
val installed: List<Extension.Installed>,
|
||||
val available: List<Extension.Available>,
|
||||
val untrusted: List<Extension.Untrusted>,
|
||||
)
|
@@ -0,0 +1,12 @@
|
||||
package eu.kanade.domain.history.interactor
|
||||
|
||||
import eu.kanade.domain.history.repository.HistoryRepository
|
||||
|
||||
class DeleteAllHistory(
|
||||
private val repository: HistoryRepository,
|
||||
) {
|
||||
|
||||
suspend fun await(): Boolean {
|
||||
return repository.deleteAllHistory()
|
||||
}
|
||||
}
|
@@ -0,0 +1,13 @@
|
||||
package eu.kanade.domain.history.interactor
|
||||
|
||||
import eu.kanade.domain.history.model.HistoryWithRelations
|
||||
import eu.kanade.domain.history.repository.HistoryRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
class GetHistory(
|
||||
private val repository: HistoryRepository,
|
||||
) {
|
||||
fun subscribe(query: String): Flow<List<HistoryWithRelations>> {
|
||||
return repository.getHistory(query)
|
||||
}
|
||||
}
|
@@ -0,0 +1,33 @@
|
||||
package eu.kanade.domain.history.interactor
|
||||
|
||||
import eu.kanade.domain.chapter.interactor.GetChapterByMangaId
|
||||
import eu.kanade.domain.chapter.model.Chapter
|
||||
import eu.kanade.domain.history.repository.HistoryRepository
|
||||
import eu.kanade.domain.manga.interactor.GetManga
|
||||
import eu.kanade.tachiyomi.util.chapter.getChapterSort
|
||||
import kotlin.math.max
|
||||
|
||||
class GetNextUnreadChapters(
|
||||
private val getChapterByMangaId: GetChapterByMangaId,
|
||||
private val getManga: GetManga,
|
||||
private val historyRepository: HistoryRepository,
|
||||
) {
|
||||
|
||||
suspend fun await(): Chapter? {
|
||||
val history = historyRepository.getLastHistory() ?: return null
|
||||
return await(history.mangaId, history.chapterId).firstOrNull()
|
||||
}
|
||||
|
||||
suspend fun await(mangaId: Long): List<Chapter> {
|
||||
val manga = getManga.await(mangaId) ?: return emptyList()
|
||||
return getChapterByMangaId.await(mangaId)
|
||||
.sortedWith(getChapterSort(manga, sortDescending = false))
|
||||
.filterNot { it.read }
|
||||
}
|
||||
|
||||
suspend fun await(mangaId: Long, fromChapterId: Long): List<Chapter> {
|
||||
val unreadChapters = await(mangaId)
|
||||
val currChapterIndex = unreadChapters.indexOfFirst { it.id == fromChapterId }
|
||||
return unreadChapters.subList(max(0, currChapterIndex), unreadChapters.size)
|
||||
}
|
||||
}
|
@@ -0,0 +1,13 @@
|
||||
package eu.kanade.domain.history.interactor
|
||||
|
||||
import eu.kanade.domain.history.model.HistoryWithRelations
|
||||
import eu.kanade.domain.history.repository.HistoryRepository
|
||||
|
||||
class RemoveHistoryById(
|
||||
private val repository: HistoryRepository,
|
||||
) {
|
||||
|
||||
suspend fun await(history: HistoryWithRelations) {
|
||||
repository.resetHistory(history.id)
|
||||
}
|
||||
}
|
@@ -0,0 +1,12 @@
|
||||
package eu.kanade.domain.history.interactor
|
||||
|
||||
import eu.kanade.domain.history.repository.HistoryRepository
|
||||
|
||||
class RemoveHistoryByMangaId(
|
||||
private val repository: HistoryRepository,
|
||||
) {
|
||||
|
||||
suspend fun await(mangaId: Long) {
|
||||
repository.resetHistoryByMangaId(mangaId)
|
||||
}
|
||||
}
|
@@ -0,0 +1,13 @@
|
||||
package eu.kanade.domain.history.interactor
|
||||
|
||||
import eu.kanade.domain.history.model.HistoryUpdate
|
||||
import eu.kanade.domain.history.repository.HistoryRepository
|
||||
|
||||
class UpsertHistory(
|
||||
private val historyRepository: HistoryRepository,
|
||||
) {
|
||||
|
||||
suspend fun await(historyUpdate: HistoryUpdate) {
|
||||
historyRepository.upsertHistory(historyUpdate)
|
||||
}
|
||||
}
|
10
app/src/main/java/eu/kanade/domain/history/model/History.kt
Normal file
10
app/src/main/java/eu/kanade/domain/history/model/History.kt
Normal file
@@ -0,0 +1,10 @@
|
||||
package eu.kanade.domain.history.model
|
||||
|
||||
import java.util.Date
|
||||
|
||||
data class History(
|
||||
val id: Long,
|
||||
val chapterId: Long,
|
||||
val readAt: Date?,
|
||||
val readDuration: Long,
|
||||
)
|
@@ -0,0 +1,9 @@
|
||||
package eu.kanade.domain.history.model
|
||||
|
||||
import java.util.Date
|
||||
|
||||
data class HistoryUpdate(
|
||||
val chapterId: Long,
|
||||
val readAt: Date,
|
||||
val sessionReadDuration: Long,
|
||||
)
|
@@ -0,0 +1,15 @@
|
||||
package eu.kanade.domain.history.model
|
||||
|
||||
import eu.kanade.domain.manga.model.MangaCover
|
||||
import java.util.Date
|
||||
|
||||
data class HistoryWithRelations(
|
||||
val id: Long,
|
||||
val chapterId: Long,
|
||||
val mangaId: Long,
|
||||
val title: String,
|
||||
val chapterNumber: Float,
|
||||
val readAt: Date?,
|
||||
val readDuration: Long,
|
||||
val coverData: MangaCover,
|
||||
)
|
@@ -0,0 +1,20 @@
|
||||
package eu.kanade.domain.history.repository
|
||||
|
||||
import eu.kanade.domain.history.model.HistoryUpdate
|
||||
import eu.kanade.domain.history.model.HistoryWithRelations
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface HistoryRepository {
|
||||
|
||||
fun getHistory(query: String): Flow<List<HistoryWithRelations>>
|
||||
|
||||
suspend fun getLastHistory(): HistoryWithRelations?
|
||||
|
||||
suspend fun resetHistory(historyId: Long)
|
||||
|
||||
suspend fun resetHistoryByMangaId(mangaId: Long)
|
||||
|
||||
suspend fun deleteAllHistory(): Boolean
|
||||
|
||||
suspend fun upsertHistory(historyUpdate: HistoryUpdate)
|
||||
}
|
35
app/src/main/java/eu/kanade/domain/library/model/Flag.kt
Normal file
35
app/src/main/java/eu/kanade/domain/library/model/Flag.kt
Normal file
@@ -0,0 +1,35 @@
|
||||
package eu.kanade.domain.library.model
|
||||
|
||||
interface Flag {
|
||||
val flag: Long
|
||||
}
|
||||
|
||||
interface Mask {
|
||||
val mask: Long
|
||||
}
|
||||
|
||||
interface FlagWithMask : Flag, Mask
|
||||
|
||||
operator fun Long.contains(other: Flag): Boolean {
|
||||
return if (other is Mask) {
|
||||
other.flag == this and other.mask
|
||||
} else {
|
||||
other.flag == this
|
||||
}
|
||||
}
|
||||
|
||||
operator fun Long.plus(other: Flag): Long {
|
||||
return if (other is Mask) {
|
||||
this and other.mask.inv() or (other.flag and other.mask)
|
||||
} else {
|
||||
this or other.flag
|
||||
}
|
||||
}
|
||||
|
||||
operator fun Flag.plus(other: Flag): Long {
|
||||
return if (other is Mask) {
|
||||
this.flag and other.mask.inv() or (other.flag and other.mask)
|
||||
} else {
|
||||
this.flag or other.flag
|
||||
}
|
||||
}
|
@@ -0,0 +1,59 @@
|
||||
package eu.kanade.domain.library.model
|
||||
|
||||
import eu.kanade.domain.category.model.Category
|
||||
|
||||
sealed class LibraryDisplayMode(
|
||||
override val flag: Long,
|
||||
) : FlagWithMask {
|
||||
|
||||
override val mask: Long = 0b00000011L
|
||||
|
||||
object CompactGrid : LibraryDisplayMode(0b00000000)
|
||||
object ComfortableGrid : LibraryDisplayMode(0b00000001)
|
||||
object List : LibraryDisplayMode(0b00000010)
|
||||
object CoverOnlyGrid : LibraryDisplayMode(0b00000011)
|
||||
|
||||
object Serializer {
|
||||
fun deserialize(serialized: String): LibraryDisplayMode {
|
||||
return LibraryDisplayMode.deserialize(serialized)
|
||||
}
|
||||
|
||||
fun serialize(value: LibraryDisplayMode): String {
|
||||
return value.serialize()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val values = setOf(CompactGrid, ComfortableGrid, List, CoverOnlyGrid)
|
||||
val default = CompactGrid
|
||||
|
||||
fun valueOf(flag: Long?): LibraryDisplayMode {
|
||||
if (flag == null) return default
|
||||
return values
|
||||
.find { mode -> mode.flag == flag and mode.mask }
|
||||
?: default
|
||||
}
|
||||
|
||||
fun deserialize(serialized: String): LibraryDisplayMode {
|
||||
return when (serialized) {
|
||||
"COMFORTABLE_GRID" -> ComfortableGrid
|
||||
"COMPACT_GRID" -> CompactGrid
|
||||
"COVER_ONLY_GRID" -> CoverOnlyGrid
|
||||
"LIST" -> List
|
||||
else -> default
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun serialize(): String {
|
||||
return when (this) {
|
||||
ComfortableGrid -> "COMFORTABLE_GRID"
|
||||
CompactGrid -> "COMPACT_GRID"
|
||||
CoverOnlyGrid -> "COVER_ONLY_GRID"
|
||||
List -> "LIST"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val Category.display: LibraryDisplayMode
|
||||
get() = LibraryDisplayMode.valueOf(flags)
|
@@ -0,0 +1,24 @@
|
||||
package eu.kanade.domain.library.model
|
||||
|
||||
import eu.kanade.domain.manga.model.Manga
|
||||
|
||||
data class LibraryManga(
|
||||
val manga: Manga,
|
||||
val category: Long,
|
||||
val totalChapters: Long,
|
||||
val readCount: Long,
|
||||
val bookmarkCount: Long,
|
||||
val latestUpload: Long,
|
||||
val chapterFetchedAt: Long,
|
||||
val lastRead: Long,
|
||||
) {
|
||||
val id: Long = manga.id
|
||||
|
||||
val unreadCount
|
||||
get() = totalChapters - readCount
|
||||
|
||||
val hasBookmarks
|
||||
get() = bookmarkCount > 0
|
||||
|
||||
val hasStarted = readCount > 0
|
||||
}
|
121
app/src/main/java/eu/kanade/domain/library/model/LibrarySort.kt
Normal file
121
app/src/main/java/eu/kanade/domain/library/model/LibrarySort.kt
Normal file
@@ -0,0 +1,121 @@
|
||||
package eu.kanade.domain.library.model
|
||||
|
||||
import eu.kanade.domain.category.model.Category
|
||||
|
||||
data class LibrarySort(
|
||||
val type: Type,
|
||||
val direction: Direction,
|
||||
) : FlagWithMask {
|
||||
|
||||
override val flag: Long
|
||||
get() = type + direction
|
||||
|
||||
override val mask: Long
|
||||
get() = type.mask or direction.mask
|
||||
|
||||
val isAscending: Boolean
|
||||
get() = direction == Direction.Ascending
|
||||
|
||||
sealed class Type(
|
||||
override val flag: Long,
|
||||
) : FlagWithMask {
|
||||
|
||||
override val mask: Long = 0b00111100L
|
||||
|
||||
object Alphabetical : Type(0b00000000)
|
||||
object LastRead : Type(0b00000100)
|
||||
object LastUpdate : Type(0b00001000)
|
||||
object UnreadCount : Type(0b00001100)
|
||||
object TotalChapters : Type(0b00010000)
|
||||
object LatestChapter : Type(0b00010100)
|
||||
object ChapterFetchDate : Type(0b00011000)
|
||||
object DateAdded : Type(0b00011100)
|
||||
|
||||
companion object {
|
||||
|
||||
fun valueOf(flag: Long): Type {
|
||||
return types.find { type -> type.flag == flag and type.mask } ?: default.type
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed class Direction(
|
||||
override val flag: Long,
|
||||
) : FlagWithMask {
|
||||
|
||||
override val mask: Long = 0b01000000L
|
||||
|
||||
object Ascending : Direction(0b01000000)
|
||||
object Descending : Direction(0b00000000)
|
||||
|
||||
companion object {
|
||||
|
||||
fun valueOf(flag: Long): Direction {
|
||||
return directions.find { direction -> direction.flag == flag and direction.mask } ?: default.direction
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object Serializer {
|
||||
fun deserialize(serialized: String): LibrarySort {
|
||||
return LibrarySort.deserialize(serialized)
|
||||
}
|
||||
|
||||
fun serialize(value: LibrarySort): String {
|
||||
return value.serialize()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val types = setOf(Type.Alphabetical, Type.LastRead, Type.LastUpdate, Type.UnreadCount, Type.TotalChapters, Type.LatestChapter, Type.ChapterFetchDate, Type.DateAdded)
|
||||
val directions = setOf(Direction.Ascending, Direction.Descending)
|
||||
val default = LibrarySort(Type.Alphabetical, Direction.Ascending)
|
||||
|
||||
fun valueOf(flag: Long): LibrarySort {
|
||||
return LibrarySort(
|
||||
Type.valueOf(flag),
|
||||
Direction.valueOf(flag),
|
||||
)
|
||||
}
|
||||
|
||||
fun deserialize(serialized: String): LibrarySort {
|
||||
if (serialized.isEmpty()) return default
|
||||
return try {
|
||||
val values = serialized.split(",")
|
||||
val type = when (values[0]) {
|
||||
"ALPHABETICAL" -> Type.Alphabetical
|
||||
"LAST_READ" -> Type.LastRead
|
||||
"LAST_MANGA_UPDATE" -> Type.LastUpdate
|
||||
"UNREAD_COUNT" -> Type.UnreadCount
|
||||
"TOTAL_CHAPTERS" -> Type.TotalChapters
|
||||
"LATEST_CHAPTER" -> Type.LatestChapter
|
||||
"CHAPTER_FETCH_DATE" -> Type.ChapterFetchDate
|
||||
"DATE_ADDED" -> Type.DateAdded
|
||||
else -> Type.Alphabetical
|
||||
}
|
||||
val ascending = if (values[1] == "ASCENDING") Direction.Ascending else Direction.Descending
|
||||
LibrarySort(type, ascending)
|
||||
} catch (e: Exception) {
|
||||
default
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun serialize(): String {
|
||||
val type = when (type) {
|
||||
Type.Alphabetical -> "ALPHABETICAL"
|
||||
Type.LastRead -> "LAST_READ"
|
||||
Type.LastUpdate -> "LAST_MANGA_UPDATE"
|
||||
Type.UnreadCount -> "UNREAD_COUNT"
|
||||
Type.TotalChapters -> "TOTAL_CHAPTERS"
|
||||
Type.LatestChapter -> "LATEST_CHAPTER"
|
||||
Type.ChapterFetchDate -> "CHAPTER_FETCH_DATE"
|
||||
Type.DateAdded -> "DATE_ADDED"
|
||||
}
|
||||
val direction = if (direction == Direction.Ascending) "ASCENDING" else "DESCENDING"
|
||||
return "$type,$direction"
|
||||
}
|
||||
}
|
||||
|
||||
val Category.sort: LibrarySort
|
||||
get() = LibrarySort.valueOf(flags)
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user