mirror of
https://github.com/mihonapp/mihon.git
synced 2025-07-27 18:05:53 +02:00
Compare commits
1177 Commits
Author | SHA1 | Date | |
---|---|---|---|
c8d68590db | |||
94448faf97 | |||
28028c789c | |||
f8834ee764 | |||
7c703b17d3 | |||
f77ade7dda | |||
91712daee8 | |||
c615f4d458 | |||
9e09a20e65 | |||
7115a9b9fe | |||
fd8b97fc87 | |||
4dd67e4348 | |||
10973bf3cd | |||
934ed0551a | |||
38428c6ebe | |||
bf85e147e7 | |||
d2dd34c2e5 | |||
c4ab2b4675 | |||
aa2ec5940f | |||
79323de326 | |||
08e6487a9a | |||
4498b10a10 | |||
6f2bb18d72 | |||
b690de55e5 | |||
83fda20078 | |||
f656a37045 | |||
c58b495433 | |||
242aeb6a68 | |||
d9969cea8a | |||
d61db5931e | |||
0ea3ac9807 | |||
f9e43f574f | |||
5ef11e61d0 | |||
48546c3db4 | |||
4d87ed496c | |||
06d12e6562 | |||
477e3d9b94 | |||
3c16082636 | |||
29aee68ec7 | |||
75e23299b4 | |||
935ff1ee98 | |||
c672cb81ec | |||
7559c133c0 | |||
589bdba0b1 | |||
aca65f13bb | |||
7bf30a094a | |||
5454279a8e | |||
006bcdf934 | |||
b00f00730d | |||
f2c48480b6 | |||
1730dd6af1 | |||
2501fef9e4 | |||
12e41b6e6f | |||
c892c793a8 | |||
3a82b4d924 | |||
b4b3a4d286 | |||
448702e5be | |||
2ef1f07aae | |||
1a319601de | |||
cdf242e8c8 | |||
aee785a8bb | |||
d45fc1e245 | |||
14500ba4f8 | |||
345e9c2a9a | |||
b53e24e0db | |||
d3a73fc228 | |||
c2812fca24 | |||
856847a60a | |||
748e2480d3 | |||
2ebc8d9ae5 | |||
e28b015580 | |||
e4bc8990fb | |||
a179327d9d | |||
823749fc1e | |||
2b5d9fd76b | |||
7a972dfdb7 | |||
c31e75f02f | |||
b56b8b55b4 | |||
2695a4d8c7 | |||
1a4dad72a9 | |||
b7e6b4c28a | |||
dc2d470413 | |||
293b967858 | |||
c637172ee0 | |||
e468554fd9 | |||
5b5eb92184 | |||
58ebf14691 | |||
992bab4f79 | |||
6fe650319d | |||
f301dc64f0 | |||
33a2219716 | |||
62480f090b | |||
e7937fe562 | |||
287489d7d0 | |||
2df0236669 | |||
c54d77333f | |||
8c494f314c | |||
8cea78de83 | |||
b6468c7e31 | |||
1967923a94 | |||
91004ad514 | |||
a2ee4e63ae | |||
4d8289cd36 | |||
289264878e | |||
768bb7b503 | |||
db4ae134aa | |||
7329f03bc5 | |||
82ea643c7d | |||
741c10e0b9 | |||
34bb90f3c2 | |||
f04cf72c0c | |||
157438e0c1 | |||
75b23c99ec | |||
6bb3070c57 | |||
7df10b076c | |||
2245658363 | |||
46774771ec | |||
6263817bb4 | |||
60456fe0e9 | |||
a0f47d3f1b | |||
6efcb8ccfa | |||
0d128b75e2 | |||
0067d474c8 | |||
cf393b217b | |||
e265b929a1 | |||
4cd01428ed | |||
3be05fbf9b | |||
5d90ba8aa0 | |||
48cab708ce | |||
5d9753d6a7 | |||
425e48bec6 | |||
a42be4a833 | |||
30e030bb8e | |||
2a3c3d8d6a | |||
7b026cec8d | |||
d8b528a4e0 | |||
0f45907144 | |||
c4c9931ae2 | |||
68345e636e | |||
0861c5618c | |||
817418f7c9 | |||
4eb2cd85b2 | |||
086eac5975 | |||
addd6bffbd | |||
1e65313fa7 | |||
c4c6e41c46 | |||
920ca405a2 | |||
6d3a3b3f39 | |||
50d46fe7f6 | |||
91e282d7e5 | |||
a0f10f868e | |||
6a423f0650 | |||
5cc84403e1 | |||
ab61a65b4a | |||
01ec26842d | |||
bbf5817805 | |||
50981cb102 | |||
611ec8103c | |||
12c672667c | |||
db3c98fe72 | |||
f401574f5a | |||
3251fb36c8 | |||
94a410f50f | |||
a14c01c1de | |||
ca3b948628 | |||
a8230ad574 | |||
8e1b5b4803 | |||
8552838bda | |||
46417fe427 | |||
dac04f2929 | |||
63da463e02 | |||
817e144ff6 | |||
9d2d78ae5b | |||
c44db54d9f | |||
376bbeb724 | |||
0e2bdb7863 | |||
235bc77457 | |||
593172f891 | |||
e20c66b156 | |||
5f4825465e | |||
bc6a12a4f7 | |||
90db3acefd | |||
2f2f59279d | |||
4992f87cb1 | |||
7608cb0da3 | |||
9dd9e741f3 | |||
171db639ff | |||
3ede42252c | |||
a94ca175e2 | |||
3749cee28f | |||
ca500da4d8 | |||
820ed6a468 | |||
7cbe18d325 | |||
8937e22ce4 | |||
82a3a98a5a | |||
d97eab0328 | |||
a61e2799db | |||
1009e15aa6 | |||
01c6e46a71 | |||
ed5e013874 | |||
f8e4153dbf | |||
f7a92cf6ac | |||
e748d91d4a | |||
2c4ddca38e | |||
6ca32710be | |||
f05e251991 | |||
a3f3f9d562 | |||
410fcb73c5 | |||
b6d6de6b9f | |||
09cebf20f3 | |||
a8c732d67b | |||
843c9c7e57 | |||
c88b79fa17 | |||
3f9820ac79 | |||
c288e6b8fa | |||
8945ef8880 | |||
99a717f849 | |||
4622b18c99 | |||
4f5270cb7d | |||
719d427956 | |||
d7a21771a5 | |||
be854b3e90 | |||
47f079891f | |||
696dc59ea5 | |||
5f6666a438 | |||
f284a656d7 | |||
1c3d566f8d | |||
373463e995 | |||
7be9b49143 | |||
059a79debb | |||
1a70ebe7ea | |||
beda99bbe0 | |||
bb1e7816e1 | |||
b0dc20e00c | |||
3d66eaea83 | |||
5313a5d5d2 | |||
5b189a909b | |||
75a687138d | |||
ba91b483a0 | |||
3a8b5e1b5e | |||
94d1b68598 | |||
8eda4df71f | |||
8ad9337863 | |||
cd13e187cf | |||
bcc21e55bd | |||
5fbecfd7b7 | |||
3480b45098 | |||
5076ab3049 | |||
44366ac058 | |||
4f2a794fba | |||
fe6aa4358f | |||
f99b62a069 | |||
ac1bed38f9 | |||
217b03a292 | |||
28bceffc6f | |||
09266a155c | |||
3d7591feca | |||
e14909fff4 | |||
fe579c4865 | |||
37118088d4 | |||
5c9e9bd2c4 | |||
db35ba53b1 | |||
758d223776 | |||
21a9bf2463 | |||
a54d9912d0 | |||
7e74949d38 | |||
a8c5780963 | |||
f4ac754d02 | |||
0347d3970a | |||
acc2312384 | |||
7d34ff214c | |||
e2179a6669 | |||
5c37347cec | |||
ef3a6c80a7 | |||
2a2c6cee5f | |||
7dff3cc6cb | |||
8c1171a722 | |||
2c850d0e33 | |||
f1b85ff39d | |||
b7fa25777d | |||
2d86f69caa | |||
e22896a956 | |||
be5802e473 | |||
434c90d378 | |||
eb6ba96b57 | |||
5325e590ec | |||
6ad6dae191 | |||
3f34fa1f58 | |||
d12ea86b55 | |||
a8e45beb51 | |||
ba2a528886 | |||
d60367768b | |||
db6528d3fa | |||
f5873d70c6 | |||
10e349f76e | |||
b1ccebf329 | |||
3407eb84c5 | |||
6017229d1b | |||
4f00af3173 | |||
9da232dcd8 | |||
acd43005df | |||
c31cf2a03a | |||
51c964de3a | |||
dad24e785b | |||
a908283e86 | |||
d8725c7b7f | |||
262f8449b4 | |||
bdf035d60a | |||
0270878748 | |||
6ada3c90ff | |||
4e628fe6de | |||
a8eebd824a | |||
afa0a0a0e2 | |||
92b039fac7 | |||
acc65529a0 | |||
3061f198e9 | |||
6fc1f4fc21 | |||
a0f49b16c5 | |||
c6c4c1c393 | |||
811931ccc0 | |||
08d5633d81 | |||
c76d5dd30c | |||
340357d158 | |||
11ed47397d | |||
6ce54eb845 | |||
d0236aaecf | |||
00059848b4 | |||
e45f6d0c92 | |||
18ccde082d | |||
21bc0f1952 | |||
a37be747e9 | |||
bc3bb82651 | |||
ba00d9e5d2 | |||
bf9edda04c | |||
9c9357639a | |||
3733871d2f | |||
54471a014f | |||
8749be518f | |||
6d880c938a | |||
34aa4eb291 | |||
280b0f42db | |||
65387d0089 | |||
d41c103a72 | |||
0b93b9e059 | |||
ea3f933e95 | |||
b006fe3a22 | |||
37ff3b4920 | |||
1e93d785e5 | |||
999bd4efee | |||
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 | |||
daaa23e8e0 | |||
1d6aa9a277 | |||
7497e02979 | |||
f34dc3be90 | |||
65261356eb | |||
4291cc8eb1 | |||
9dbc1aa7a3 | |||
b0520df1dd | |||
5f378e28b6 | |||
4ebceac07f | |||
aab5a56892 | |||
83129385e2 | |||
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 | |||
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 |
.github
.gitignoreREADME.mdapp
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
prefs
util
data
domain
DomainModule.kt
backup
service
base
category
interactor
chapter
interactor
GetChapter.ktGetChapterByMangaId.ktSetDefaultChapterSettings.ktSetReadStatus.ktSyncChaptersWithSource.ktSyncChaptersWithTrackServiceTwoWay.ktUpdateChapter.kt
model
download
extension
history
interactor
library
service
manga
interactor
GetDuplicateLibraryManga.ktGetFavorites.ktGetLibraryManga.ktGetManga.ktGetMangaWithChapters.ktNetworkToLocalManga.ktResetViewerFlags.ktSetMangaChapterFlags.ktSetMangaViewerFlags.ktUpdateManga.kt
model
source
interactor
GetEnabledSources.ktGetLanguagesWithSources.ktGetRemoteManga.ktGetSourcesWithFavoriteCount.ktGetSourcesWithNonLibraryManga.ktSetMigrateSorting.ktToggleLanguage.ktToggleSource.ktToggleSourcePin.kt
model
repository
service
track
interactor
model
service
store
ui
presentation
browse
BrowseBadges.ktBrowseSourceScreen.ktExtensionDetailsScreen.ktExtensionFilterScreen.ktExtensionsScreen.ktGlobalSearchScreen.ktMigrateMangaScreen.ktMigrateSearchScreen.ktMigrateSourceScreen.ktSourcesFilterScreen.ktSourcesScreen.kt
components
category
components
AdaptiveSheet.ktAlertDialog.ktAppBar.ktBadges.ktBanners.ktButton.ktChangeCategoryDialog.ktChapterDownloadIndicator.ktCommonMangaItem.ktDeleteLibraryMangaDialog.ktDivider.ktDownloadDropdownMenu.ktDropdownMenu.ktDuplicateMangaDialog.ktEmptyScreen.ktFloatingActionButton.ktIconButton.ktInfoScaffold.ktLazyGrid.ktLazyList.ktLinkIcon.ktListGroupHeader.ktLoadingScreen.ktMangaBottomActionMenu.ktMangaCover.ktNavigationBar.ktNavigationRail.ktPager.ktPill.ktPullRefresh.ktRelativeDateHeader.ktScaffold.ktSettingsItems.ktSurface.ktTabbedDialog.ktTabbedScreen.ktTabs.ktTrackLogoIcon.ktTwoPanelBox.ktVerticalFastScroller.kt
crash
history
library
components
manga
ChapterSettingsDialog.ktMangaScreen.ktMangaScreenConstants.ktTrackInfoDialogHome.ktTrackInfoDialogSelector.ktTrackServiceSearch.kt
components
more
LogoHeader.ktMoreScreen.ktNewUpdateScreen.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.ktWorkerInfoScreen.kt
widget
stats
theme
updates
util
Constants.ktElevation.ktLazyListState.ktModifier.ktNavigator.ktPaddingValues.ktPreference.ktPreview.ktResources.ktScrollable.ktScrollbar.ktWindowSize.kt
webview
tachiyomi
App.ktAppInfo.ktAppModule.ktMigrations.kt
crash
data
backup
AbstractBackupManager.ktAbstractBackupRestore.ktAbstractBackupRestoreValidator.ktBackupConst.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.ktSourceIdMangaCount.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.ktMangaTitlePutResolver.ktSourceIdMangaCountGetResolver.kt
tables
download
DownloadCache.ktDownloadManager.ktDownloadNotifier.ktDownloadPendingDeleter.ktDownloadProvider.ktDownloadService.ktDownloadStore.ktDownloader.kt
model
library
notification
preference
saver
track
EnhancedTrackService.ktNoLoginTrackService.ktTrackManager.ktTrackService.kt
anilist
bangumi
Avatar.ktBangumi.ktBangumiApi.ktBangumiInterceptor.ktBangumiModels.ktCollection.ktOAuth.ktStatus.ktUser.kt
job
kavita
kitsu
komga
mangaupdates
model
myanimelist
shikimori
suwayomi
updater
extension
network
source
ui
base
activity
controller
BaseController.ktConductorExtensions.ktDialogController.ktFabController.ktNoAppBarElevationController.ktNucleusController.ktOneWayFadeChangeHandler.ktRootController.ktRxController.ktSearchableNucleusController.ktTabbedController.kt
delegate
presenter
browse
BrowseController.ktBrowseTab.kt
extension
ExtensionAdapter.ktExtensionController.ktExtensionFilterController.ktExtensionFilterScreen.ktExtensionFilterScreenModel.ktExtensionGroupHolder.ktExtensionGroupItem.ktExtensionHolder.ktExtensionItem.ktExtensionPresenter.ktExtensionTrustDialog.ktExtensionViewUtils.ktExtensionsScreenModel.ktExtensionsTab.kt
details
migration
MigrationFlags.kt
manga
MigrationMangaAdapter.ktMigrationMangaController.ktMigrationMangaHolder.ktMigrationMangaItem.ktMigrationMangaPresenter.ktMigrationMangaScreen.ktMigrationMangaScreenModel.kt
search
MigrateDialog.ktMigrateSearchScreen.ktMigrateSearchScreenModel.ktSearchController.ktSearchPresenter.ktSourceSearchController.ktSourceSearchScreen.kt
sources
source
LangHolder.ktLangItem.ktSourceAdapter.ktSourceController.ktSourceFilterController.ktSourceHolder.ktSourceItem.ktSourcePresenter.ktSourcesFilterScreen.ktSourcesFilterScreenModel.ktSourcesScreenModel.ktSourcesTab.kt
browse
BrowseSourceController.ktBrowseSourcePresenter.ktBrowseSourceScreen.ktBrowseSourceScreenModel.ktNoResultsException.ktPager.ktProgressItem.ktSourceComfortableGridHolder.ktSourceCompactGridHolder.ktSourceFilterSheet.ktSourceHolder.ktSourceItem.ktSourceListHolder.ktSourcePager.kt
globalsearch
GlobalSearchAdapter.ktGlobalSearchCardAdapter.ktGlobalSearchCardHolder.ktGlobalSearchCardItem.ktGlobalSearchController.ktGlobalSearchHolder.ktGlobalSearchItem.ktGlobalSearchPresenter.ktGlobalSearchScreen.ktGlobalSearchScreenModel.ktSearchScreenModel.kt
latest
category
CategoryAdapter.ktCategoryController.ktCategoryCreateDialog.ktCategoryHolder.ktCategoryItem.ktCategoryPresenter.ktCategoryRenameDialog.ktCategoryScreen.ktCategoryScreenModel.kt
download
DownloadAdapter.ktDownloadController.ktDownloadHeaderItem.ktDownloadItem.ktDownloadPresenter.ktDownloadQueueScreen.ktDownloadQueueScreenModel.kt
history
home
library
ChangeMangaCategoriesDialog.ktChangeMangaCoverDialog.ktDeleteLibraryMangasDialog.ktLibraryAdapter.ktLibraryCategoryAdapter.ktLibraryCategoryView.ktLibraryComfortableGridHolder.ktLibraryCompactGridHolder.ktLibraryController.ktLibraryHolder.ktLibraryItem.ktLibraryListHolder.ktLibraryMangaEvent.ktLibraryPresenter.ktLibraryScreenModel.ktLibrarySelectionEvent.ktLibrarySettingsSheet.ktLibraryTab.kt
setting
main
manga
AddDuplicateMangaDialog.ktMangaController.ktMangaCoverScreenModel.ktMangaPresenter.ktMangaScreen.ktMangaScreenModel.kt
chapter
ChapterDownloadView.ktChapterHolder.ktChapterItem.ktChaptersAdapter.ktChaptersSettingsSheet.ktDeleteChaptersDialog.ktDownloadCustomChaptersDialog.ktMangaChaptersHeaderAdapter.ktSetChapterSettingsDialog.kt
base
info
track
more
AboutController.ktAboutLinksPreference.ktMoreController.ktMoreHeaderPreference.ktMoreTab.ktNewUpdateDialogController.ktNewUpdateScreen.kt
licenses
reader
ReaderActivity.ktReaderNavigationOverlayView.ktReaderPageSheet.ktReaderPresenter.ktReaderViewModel.ktSaveImageNotifier.kt
loader
ChapterLoader.ktDirectoryPageLoader.ktDownloadPageLoader.ktEpubPageLoader.ktHttpPageLoader.ktPageLoader.ktRarPageLoader.ktZipPageLoader.kt
model
setting
OrientationType.ktReaderColorFilterSettings.ktReaderGeneralSettings.ktReaderPreferences.ktReaderReadingModeSettings.kt
viewer
recent
DateSectionItem.kt
history
HistoryAdapter.ktHistoryController.ktHistoryHolder.ktHistoryItem.ktHistoryPresenter.ktRemoveHistoryDialog.kt
updates
security
setting
SettingsAdvancedController.ktSettingsAppearanceController.ktSettingsBackupController.ktSettingsBrowseController.ktSettingsController.ktSettingsDownloadController.ktSettingsGeneralController.ktSettingsLibraryController.ktSettingsMainController.ktSettingsReaderController.ktSettingsScreen.ktSettingsSecurityController.ktSettingsTrackingController.kt
database
search
SettingsSearchAdapter.ktSettingsSearchController.ktSettingsSearchHelper.ktSettingsSearchHolder.ktSettingsSearchItem.ktSettingsSearchPresenter.kt
track
stats
updates
webview
util
BackupUtil.ktCrashLogUtil.ktMangaExtensions.kt
chapter
ChapterGetNextUnread.ktChapterRecognition.ktChapterSettingsHelper.ktChapterSorter.ktChapterSourceSync.ktChapterTrackSync.kt
lang
preference
storage
system
AuthenticatorUtil.ktBuildConfig.ktContextExtensions.ktDeviceUtilExtensions.ktImageUtil.ktIntentExtensions.ktInternalResourceHelper.ktLocaleHelper.ktNotificationExtensions.kt
view
widget
ActionModeWithToolbar.ktAutofitRecyclerView.ktDialogCheckboxView.ktDialogCustomDownloadView.ktEmptyView.ktExtendedNavigationView.ktHideBottomNavigationOnScrollBehavior.ktMangaSummaryView.ktMaterialFastScroll.ktMaterialSpinnerView.ktMinMaxNumberPicker.ktRecyclerViewPagerAdapter.ktTachiyomiAppBarLayout.ktTachiyomiBottomNavigationView.ktTachiyomiChangeHandlerFrameLayout.ktTachiyomiCoordinatorLayout.ktTachiyomiFullscreenDialog.ktTachiyomiScrollingViewBehavior.ktTachiyomiSearchView.ktTachiyomiTextInputEditText.ktThemedSwipeRefreshLayout.kt
materialdialogs
MaterialAlertDialogBuilderExtensions.ktQuadStateMultiChoiceDialogAdapter.ktQuadStateMultiChoiceViewHolder.ktQuadStateTextView.kt
preference
res
anim-v33
shared_axis_x_pop_enter.xmlshared_axis_x_pop_exit.xmlshared_axis_x_push_enter.xmlshared_axis_x_push_exit.xml
anim
fade_in_short.xmlfade_out_short.xmlshared_axis_x_pop_enter.xmlshared_axis_x_pop_exit.xmlshared_axis_x_push_enter.xmlshared_axis_x_push_exit.xml
color-night-v31
color-v31
color
button_action_selector.xmllibrary_item_background.xmllibrary_item_foreground.xmlsource_comfortable_item_title.xml
drawable-nodpi
drawable-v26
drawable
anim_browse_leave.xmlanim_caret_up.xmlanim_library_leave.xmlanim_more_enter.xmlanim_updates_leave.xmlcard_gradient_shape.xmlic_add_24dp.xmlic_arrow_back_24dp.xmlic_arrow_downward_24dp.xmlic_arrow_forward_24dp.xmlic_browse_filled_24dp.xmlic_browse_outline_24dp.xmlic_browse_selector_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_delete_sweep_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_history_24dp.xmlic_history_selector_24dp.xmlic_indeterminate_check_box_24dp.xmlic_label_24dp.xmlic_library_filled_24dp.xmlic_library_outline_24dp.xmlic_library_selector_24dp.xmlic_more_24dp.xmlic_more_selector_24dp.xmlic_more_vert_24.xmlic_palette_24dp.xmlic_public_24dp.xmlic_push_pin_24dp.xmlic_push_pin_outline_24dp.xmlic_search_24dp.xmlic_security_24dp.xmlic_select_all_24dp.xmlic_settings_backup_restore_24dp.xmlic_sort_24dp.xmlic_status_completed_24dp.xmlic_status_licensed_24dp.xmlic_status_ongoing_24dp.xmlic_status_unknown_24dp.xmlic_sync_24dp.xmlic_translate_24dp.xmlic_travel_explore_24dp.xmlic_tune_24dp.xmlic_updates_filled_24dp.xmlic_updates_outline_24dp.xmlic_updates_selector_24dp.xmlic_view_module_24dp.xmlic_webview_24dp.xmllibrary_item_selector.xmllibrary_item_selector_overlay.xmllist_item_selector_background.xmlmanga_backdrop_gradient.xmlmanga_info_gradient.xmlmanga_info_more_gradient.xmlmaterial_popup_background.xmloval.xmlsc_collections_bookmark_48dp.xmlsc_explore_48dp.xmlsc_history_48dp.xmlsc_new_releases_48dp.xml
layout-sw720dp
layout
action_toolbar.xmlcategories_controller.xmlcategories_item.xmlchapter_download_view.xmlchapters_item.xmlclear_database_controller.xmlclear_database_source_item.xmlcommon_dialog_with_checkbox.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_controller.xmlextension_detail_controller.xmlextension_detail_header.xmlextension_item.xmlglobal_search_controller.xmlglobal_search_controller_card.xmlglobal_search_controller_card_item.xmlhistory_controller.xmlhistory_item.xmllibrary_category.xmllibrary_controller.xmllibrary_grid_recycler.xmllibrary_list_recycler.xmllicenses_controller.xmllicenses_item.xmlmain_activity.xmlmain_activity_fab.xmlmanga_chapters_header.xmlmanga_controller.xmlmanga_full_cover_dialog.xmlmanga_info_header.xmlmanga_summary.xmlmigration_manga_controller.xmlmigration_sources_controller.xmlpager_controller.xmlpref_about_links.xmlpref_account_login.xmlpref_library_columns.xmlpref_more_header.xmlpref_settings.xmlpref_theme_item.xmlpref_themes_list.xmlpref_tracker_item.xmlreader_activity.xmlreader_pager_settings.xmlreader_webtoon_settings.xmlsection_header_item.xmlsettings_search_controller.xmlsettings_search_controller_card.xmlsource_comfortable_grid_item.xmlsource_compact_grid_item.xmlsource_controller.xmlsource_grid_item_badges.xmlsource_list_item.xmlsource_main_controller_item.xmlsource_progress_item.xmlsource_recycler_autofit.xmltrack_chapters_dialog.xmltrack_controller.xmltrack_item.xmltrack_score_dialog.xmltrack_search_dialog.xmltrack_search_item.xmlupdates_controller.xmlupdates_item.xmlwebview_activity.xml
menu
browse_extensions.xmlbrowse_migrate.xmlbrowse_sources.xmlcategory_selection.xmlchapter_download.xmlchapter_selection.xmldefault_chapter_filter.xmldownload_queue.xmlextension_details.xmlfull_cover.xmlgeneric_selection.xmlglobal_search.xmlhistory.xmllibrary.xmllibrary_selection.xmlmain_nav.xmlmanga.xmlreader.xmlsettings_backup.xmlsettings_main.xmlsettings_tracking.xmlsource_browse.xmltrack_item.xmltrack_item_date.xmlupdates.xmlupdates_chapter_selection.xmlwebview.xml
mipmap-anydpi-v26
mipmap-hdpi
mipmap-mdpi
mipmap-xhdpi
mipmap-xxhdpi
mipmap-xxxhdpi
values-aii
values-gl
values-jv
values-kk
values-lt
values-lv
values-night
values-or
values-sk
values-sr
values-sw600dp-port
values-sw720dp
values-tl
values-uz
values
xml
test
java
eu
kanade
tachiyomi
util
chapter
buildSrc
core
.gitignorebuild.gradle.kts
src
main
AndroidManifest.xml
java
eu
kanade
tachiyomi
tachiyomi
data
.gitignorebuild.gradle.ktsconsumer-rules.proproguard-rules.pro
src
main
AndroidManifest.xml
java
tachiyomi
data
AndroidDatabaseHandler.ktDatabaseAdapter.ktDatabaseHandler.ktQueryPagingSource.ktTransactionContext.kt
category
chapter
history
manga
source
track
updates
sqldelight
domain
.gitignorebuild.gradle.ktsconsumer-rules.proproguard-rules.pro
gradle.propertiessrc
main
AndroidManifest.xml
java
tachiyomi
domain
category
chapter
interactor
model
repository
history
interactor
model
repository
library
manga
source
track
updates
test
java
tachiyomi
domain
library
model
gradle
gradlewgradlew.bati18n
.gitignorebuild.gradle.kts
src
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-sq
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
presentation-core
presentation-widget
.gitignorebuild.gradle.ktsconsumer-rules.proproguard-rules.pro
settings.gradle.ktssrc
main
source-api
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.13.6)
|
||||
- To the latest version of the app (stable is v0.14.7)
|
||||
- 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.
|
||||
|
8
.github/ISSUE_TEMPLATE/report_issue.yml
vendored
8
.github/ISSUE_TEMPLATE/report_issue.yml
vendored
@ -45,7 +45,7 @@ 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
|
||||
@ -53,7 +53,7 @@ body:
|
||||
label: Tachiyomi version
|
||||
description: You can find your Tachiyomi version in **More → About**.
|
||||
placeholder: |
|
||||
Example: "0.13.6"
|
||||
Example: "0.14.7"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@ -90,7 +90,7 @@ body:
|
||||
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.
|
||||
- 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
|
||||
@ -98,7 +98,7 @@ body:
|
||||
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.13.6](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**.
|
||||
- label: I have updated the app to version **[0.14.7](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**.
|
||||
required: true
|
||||
- label: I have updated all installed extensions.
|
||||
required: true
|
||||
|
4
.github/ISSUE_TEMPLATE/request_feature.yml
vendored
4
.github/ISSUE_TEMPLATE/request_feature.yml
vendored
@ -27,13 +27,13 @@ body:
|
||||
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.
|
||||
- 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.13.6](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**.
|
||||
- label: I have updated the app to version **[0.14.7](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/renovate.json
vendored
Normal file
12
.github/renovate.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"extends": [
|
||||
"config:base"
|
||||
],
|
||||
"schedule": ["every sunday"],
|
||||
"ignoreDeps": [
|
||||
"androidx.core:core-splashscreen",
|
||||
"com.android.tools:r8",
|
||||
"com.google.guava:guava",
|
||||
"com.github.commandiron:WheelPickerCompose"
|
||||
]
|
||||
}
|
6
.github/workflows/build_pull_request.yml
vendored
6
.github/workflows/build_pull_request.yml
vendored
@ -3,7 +3,7 @@ on:
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
- 'app/src/main/res/**/strings.xml'
|
||||
- 'i18n/src/main/res/**/strings.xml'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
|
||||
@ -25,7 +25,7 @@ jobs:
|
||||
uses: gradle/wrapper-validation-action@v1
|
||||
|
||||
- name: Dependency Review
|
||||
uses: actions/dependency-review-action@v2
|
||||
uses: actions/dependency-review-action@v3
|
||||
|
||||
- name: Set up JDK 11
|
||||
uses: actions/setup-java@v3
|
||||
@ -36,4 +36,4 @@ jobs:
|
||||
- name: Build app and run unit tests
|
||||
uses: gradle/gradle-command-action@v2
|
||||
with:
|
||||
arguments: assembleStandardRelease testStandardReleaseUnitTest
|
||||
arguments: lintKotlin assembleStandardRelease testStandardReleaseUnitTest
|
20
.github/workflows/build_push.yml
vendored
20
.github/workflows/build_push.yml
vendored
@ -7,7 +7,7 @@ on:
|
||||
- v*
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
@ -31,7 +31,7 @@ jobs:
|
||||
- name: Build app and run unit tests
|
||||
uses: gradle/gradle-command-action@v2
|
||||
with:
|
||||
arguments: assembleStandardRelease testStandardReleaseUnitTest
|
||||
arguments: lintKotlin assembleStandardRelease testStandardReleaseUnitTest
|
||||
|
||||
# Sign APK and create release for tags
|
||||
|
||||
@ -71,6 +71,10 @@ jobs:
|
||||
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'
|
||||
@ -89,12 +93,24 @@ jobs:
|
||||
| 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 }}
|
||||
|
||||
update-website:
|
||||
needs: [build]
|
||||
runs-on: ubuntu-latest
|
||||
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'tachiyomiorg/tachiyomi'
|
||||
steps:
|
||||
- name: Trigger Netlify build hook
|
||||
run: curl -s -X POST -d {} "https://api.netlify.com/build_hooks/${TOKEN}"
|
||||
env:
|
||||
TOKEN: ${{ secrets.NETLIFY_HOOK_RELEASE }}
|
||||
|
2
.github/workflows/lock.yml
vendored
2
.github/workflows/lock.yml
vendored
@ -12,7 +12,7 @@ jobs:
|
||||
lock:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/lock-threads@v3
|
||||
- uses: dessant/lock-threads@v4
|
||||
with:
|
||||
github-token: ${{ github.token }}
|
||||
issue-inactive-days: '2'
|
||||
|
4
.gitignore
vendored
4
.gitignore
vendored
@ -12,5 +12,5 @@
|
||||
*.apk
|
||||
app/**/output.json
|
||||
|
||||
# Hebrew assets are copied on build
|
||||
app/src/main/res/values-iw/
|
||||
# Unnecessary file
|
||||
*.swp
|
@ -12,7 +12,7 @@ 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
|
||||
|
@ -1,5 +1,5 @@
|
||||
import org.gradle.api.tasks.testing.logging.TestLogEvent
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
import org.jmailen.gradle.kotlinter.tasks.LintTask
|
||||
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
@ -15,19 +15,15 @@ if (gradle.startParameter.taskRequests.toString().contains("Standard")) {
|
||||
|
||||
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
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "eu.kanade.tachiyomi"
|
||||
minSdk = AndroidConfig.minSdk
|
||||
targetSdk = AndroidConfig.targetSdk
|
||||
versionCode = 82
|
||||
versionName = "0.13.6"
|
||||
versionCode = 102
|
||||
versionName = "0.14.7"
|
||||
|
||||
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
||||
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
|
||||
@ -58,6 +54,7 @@ android {
|
||||
named("debug") {
|
||||
versionNameSuffix = "-${getCommitCount()}"
|
||||
applicationIdSuffix = ".debug"
|
||||
isPseudoLocalesEnabled = true
|
||||
}
|
||||
named("release") {
|
||||
isShrinkResources = true
|
||||
@ -72,11 +69,22 @@ android {
|
||||
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("preview").res.srcDirs("src/debug/res")
|
||||
getByName("benchmark").res.srcDirs("src/debug/res")
|
||||
}
|
||||
|
||||
flavorDimensions.add("default")
|
||||
@ -87,7 +95,8 @@ android {
|
||||
dimension = "default"
|
||||
}
|
||||
create("dev") {
|
||||
resourceConfigurations.addAll(listOf("en", "xxhdpi"))
|
||||
// Include pseudolocales: https://developer.android.com/guide/topics/resources/pseudolocales
|
||||
resourceConfigurations.addAll(listOf("en", "en_XA", "ar_XB", "xxhdpi"))
|
||||
dimension = "default"
|
||||
}
|
||||
}
|
||||
@ -101,7 +110,6 @@ android {
|
||||
"META-INF/README.md",
|
||||
"META-INF/NOTICE",
|
||||
"META-INF/*.kotlin_module",
|
||||
"META-INF/*.version",
|
||||
))
|
||||
}
|
||||
|
||||
@ -111,6 +119,7 @@ android {
|
||||
|
||||
buildFeatures {
|
||||
viewBinding = true
|
||||
compose = true
|
||||
|
||||
// Disable some unused things
|
||||
aidl = false
|
||||
@ -119,29 +128,51 @@ android {
|
||||
}
|
||||
|
||||
lint {
|
||||
disable.addAll(listOf("MissingTranslation", "ExtraTranslation"))
|
||||
abortOnError = false
|
||||
checkReleaseBuilds = false
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_1_8.toString()
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion = compose.versions.compiler.get()
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":i18n"))
|
||||
implementation(project(":core"))
|
||||
implementation(project(":source-api"))
|
||||
implementation(project(":data"))
|
||||
implementation(project(":domain"))
|
||||
implementation(project(":presentation-core"))
|
||||
implementation(project(":presentation-widget"))
|
||||
|
||||
// Compose
|
||||
implementation(platform(compose.bom))
|
||||
implementation(compose.activity)
|
||||
implementation(compose.foundation)
|
||||
implementation(compose.material3.core)
|
||||
implementation(compose.material.core)
|
||||
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.flowlayout)
|
||||
implementation(compose.accompanist.permissions)
|
||||
implementation(compose.accompanist.themeadapter)
|
||||
implementation(compose.accompanist.systemuicontroller)
|
||||
|
||||
implementation(androidx.paging.runtime)
|
||||
implementation(androidx.paging.compose)
|
||||
|
||||
implementation(libs.bundles.sqlite)
|
||||
|
||||
implementation(kotlinx.reflect)
|
||||
|
||||
implementation(platform(kotlinx.coroutines.bom))
|
||||
implementation(kotlinx.bundles.coroutines)
|
||||
|
||||
// Source models and interfaces from Tachiyomi 1.x
|
||||
implementation(libs.tachiyomi.api)
|
||||
|
||||
// AndroidX libraries
|
||||
implementation(androidx.annotation)
|
||||
implementation(androidx.appcompat)
|
||||
@ -151,8 +182,8 @@ dependencies {
|
||||
implementation(androidx.corektx)
|
||||
implementation(androidx.splashscreen)
|
||||
implementation(androidx.recyclerview)
|
||||
implementation(androidx.swiperefreshlayout)
|
||||
implementation(androidx.viewpager)
|
||||
implementation(androidx.profileinstaller)
|
||||
|
||||
implementation(androidx.bundles.lifecycle)
|
||||
|
||||
@ -173,9 +204,6 @@ dependencies {
|
||||
// Data serialization (JSON, protobuf)
|
||||
implementation(kotlinx.bundles.serialization)
|
||||
|
||||
// JavaScript engine
|
||||
implementation(libs.bundles.js.engine)
|
||||
|
||||
// HTML parser
|
||||
implementation(libs.jsoup)
|
||||
|
||||
@ -184,24 +212,14 @@ dependencies {
|
||||
implementation(libs.unifile)
|
||||
implementation(libs.junrar)
|
||||
|
||||
// Database
|
||||
implementation(libs.bundles.sqlite)
|
||||
implementation("com.github.inorichi.storio:storio-common:8be19de@aar")
|
||||
implementation("com.github.inorichi.storio:storio-sqlite:8be19de@aar")
|
||||
|
||||
// Preferences
|
||||
implementation(libs.preferencektx)
|
||||
implementation(libs.flowpreferences)
|
||||
|
||||
// Model View Presenter
|
||||
implementation(libs.bundles.nucleus)
|
||||
|
||||
// Dependency injection
|
||||
implementation(libs.injekt.core)
|
||||
|
||||
// Image loading
|
||||
implementation(libs.bundles.coil)
|
||||
|
||||
implementation(libs.subsamplingscaleimageview) {
|
||||
exclude(module = "image-decoder")
|
||||
}
|
||||
@ -212,22 +230,19 @@ dependencies {
|
||||
|
||||
// UI libraries
|
||||
implementation(libs.material)
|
||||
implementation(libs.androidprocessbutton)
|
||||
implementation(libs.flexible.adapter.core)
|
||||
implementation(libs.flexible.adapter.ui)
|
||||
implementation(libs.viewstatepageradapter)
|
||||
implementation(libs.photoview)
|
||||
implementation(libs.directionalviewpager) {
|
||||
exclude(group = "androidx.viewpager", module = "viewpager")
|
||||
}
|
||||
implementation(libs.insetter)
|
||||
implementation(libs.markwon)
|
||||
|
||||
// Conductor
|
||||
implementation(libs.bundles.conductor)
|
||||
|
||||
// FlowBinding
|
||||
implementation(libs.bundles.flowbinding)
|
||||
implementation(libs.bundles.richtext)
|
||||
implementation(libs.aboutLibraries.compose)
|
||||
implementation(libs.cascade)
|
||||
implementation(libs.bundles.voyager)
|
||||
implementation(libs.wheelpicker)
|
||||
implementation(libs.materialmotion.core)
|
||||
|
||||
// Logging
|
||||
implementation(libs.logcat)
|
||||
@ -236,9 +251,6 @@ dependencies {
|
||||
implementation(libs.acra.http)
|
||||
"standardImplementation"(libs.firebase.analytics)
|
||||
|
||||
// Licenses
|
||||
implementation(libs.aboutlibraries.core)
|
||||
|
||||
// Shizuku
|
||||
implementation(libs.bundles.shizuku)
|
||||
|
||||
@ -250,37 +262,57 @@ dependencies {
|
||||
implementation(libs.leakcanary.plumber)
|
||||
}
|
||||
|
||||
tasks {
|
||||
withType<Test> {
|
||||
useJUnitPlatform()
|
||||
testLogging {
|
||||
events(TestLogEvent.PASSED, TestLogEvent.SKIPPED, TestLogEvent.FAILED)
|
||||
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<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(
|
||||
"-opt-in=kotlin.Experimental",
|
||||
"-opt-in=kotlin.RequiresOptIn",
|
||||
"-opt-in=kotlin.ExperimentalStdlibApi",
|
||||
"-opt-in=kotlinx.coroutines.FlowPreview",
|
||||
"-opt-in=coil.annotation.ExperimentalCoilApi",
|
||||
"-opt-in=com.google.accompanist.permissions.ExperimentalPermissionsApi",
|
||||
"-opt-in=androidx.compose.foundation.layout.ExperimentalLayoutApi",
|
||||
"-opt-in=androidx.compose.material.ExperimentalMaterialApi",
|
||||
"-opt-in=androidx.compose.material3.ExperimentalMaterial3Api",
|
||||
"-opt-in=androidx.compose.material.ExperimentalMaterialApi",
|
||||
"-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",
|
||||
"-opt-in=coil.annotation.ExperimentalCoilApi",
|
||||
)
|
||||
}
|
||||
|
||||
// Duplicating Hebrew string assets due to some locale code issues on different devices
|
||||
val copyHebrewStrings by registering(Copy::class) {
|
||||
from("./src/main/res/values-he")
|
||||
into("./src/main/res/values-iw")
|
||||
include("**/*")
|
||||
}
|
||||
|
||||
preBuild {
|
||||
dependsOn(formatKotlin, copyHebrewStrings)
|
||||
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"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,3 @@
|
||||
-allowaccessmodification
|
||||
-dontusemixedcaseclassnames
|
||||
-verbose
|
||||
|
||||
|
52
app/proguard-rules.pro
vendored
52
app/proguard-rules.pro
vendored
@ -1,20 +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.**
|
||||
|
||||
@ -34,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
|
||||
@ -70,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,4 +63,7 @@
|
||||
-keepclassmembers class kotlinx.serialization.** {
|
||||
<methods>;
|
||||
}
|
||||
##---------------End: proguard configuration for kotlinx.serialization ----------
|
||||
##---------------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,5 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<!-- Internet -->
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
@ -21,6 +22,13 @@
|
||||
<!-- To view extension packages in API 30+ -->
|
||||
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
|
||||
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.READ_APP_SPECIFIC_LOCALES" />
|
||||
|
||||
<!-- 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"
|
||||
@ -28,12 +36,18 @@
|
||||
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"
|
||||
@ -48,6 +62,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"
|
||||
@ -167,6 +187,20 @@
|
||||
android:name=".data.notification.NotificationReceiver"
|
||||
android:exported="false" />
|
||||
|
||||
<receiver
|
||||
android:name="tachiyomi.presentation.widget.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" />
|
||||
@ -186,6 +220,15 @@
|
||||
<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"
|
||||
@ -204,11 +247,18 @@
|
||||
android:exported="true"
|
||||
android:permission="android.permission.INTERACT_ACROSS_USERS_FULL" />
|
||||
|
||||
<meta-data android:name="android.webkit.WebView.EnableSafeBrowsing"
|
||||
<meta-data
|
||||
android:name="android.webkit.WebView.EnableSafeBrowsing"
|
||||
android:value="false" />
|
||||
<meta-data android:name="android.webkit.WebView.MetricsOptOut"
|
||||
<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>
|
||||
|
22513
app/src/main/baseline-prof.txt
Normal file
22513
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,38 @@
|
||||
package eu.kanade.core.prefs
|
||||
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import tachiyomi.core.preference.Preference
|
||||
|
||||
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) }
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> Preference<T>.asState(scope: CoroutineScope) = PreferenceMutableState(this, scope)
|
148
app/src/main/java/eu/kanade/core/util/CollectionUtils.kt
Normal file
148
app/src/main/java/eu/kanade/core/util/CollectionUtils.kt
Normal file
@ -0,0 +1,148 @@
|
||||
package eu.kanade.core.util
|
||||
|
||||
import androidx.compose.ui.util.fastForEach
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import kotlin.contracts.ExperimentalContracts
|
||||
import kotlin.contracts.contract
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new map containing only the key entries of [transform] that are not null.
|
||||
*/
|
||||
inline fun <K, V, R> Map<out K, V>.mapNotNullKeys(transform: (Map.Entry<K?, V>) -> R?): ConcurrentHashMap<R, V> {
|
||||
val mutableMap = ConcurrentHashMap<R, V>()
|
||||
forEach { element -> transform(element)?.let { mutableMap[it] = element.value } }
|
||||
return mutableMap
|
||||
}
|
||||
|
||||
fun <E> HashSet<E>.addOrRemove(value: E, shouldAdd: Boolean) {
|
||||
if (shouldAdd) {
|
||||
add(value)
|
||||
} else {
|
||||
remove(value)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list containing only elements matching the given [predicate].
|
||||
*
|
||||
* **Do not use for collections that come from public APIs**, since they may not support random
|
||||
* access in an efficient way, and this method may actually be a lot slower. Only use for
|
||||
* collections that are created by code we control and are known to support random access.
|
||||
*/
|
||||
@OptIn(ExperimentalContracts::class)
|
||||
inline fun <T> List<T>.fastFilter(predicate: (T) -> Boolean): List<T> {
|
||||
contract { callsInPlace(predicate) }
|
||||
val destination = ArrayList<T>()
|
||||
fastForEach { if (predicate(it)) destination.add(it) }
|
||||
return destination
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list containing all elements not matching the given [predicate].
|
||||
*
|
||||
* **Do not use for collections that come from public APIs**, since they may not support random
|
||||
* access in an efficient way, and this method may actually be a lot slower. Only use for
|
||||
* collections that are created by code we control and are known to support random access.
|
||||
*/
|
||||
@OptIn(ExperimentalContracts::class)
|
||||
inline fun <T> List<T>.fastFilterNot(predicate: (T) -> Boolean): List<T> {
|
||||
contract { callsInPlace(predicate) }
|
||||
val destination = ArrayList<T>()
|
||||
fastForEach { if (!predicate(it)) destination.add(it) }
|
||||
return destination
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list containing only the non-null results of applying the
|
||||
* given [transform] function to each element in the original collection.
|
||||
*
|
||||
* **Do not use for collections that come from public APIs**, since they may not support random
|
||||
* access in an efficient way, and this method may actually be a lot slower. Only use for
|
||||
* collections that are created by code we control and are known to support random access.
|
||||
*/
|
||||
@OptIn(ExperimentalContracts::class)
|
||||
inline fun <T, R> List<T>.fastMapNotNull(transform: (T) -> R?): List<R> {
|
||||
contract { callsInPlace(transform) }
|
||||
val destination = ArrayList<R>()
|
||||
fastForEach { element ->
|
||||
transform(element)?.let { destination.add(it) }
|
||||
}
|
||||
return destination
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits the original collection into pair of lists,
|
||||
* where *first* list contains elements for which [predicate] yielded `true`,
|
||||
* while *second* list contains elements for which [predicate] yielded `false`.
|
||||
*
|
||||
* **Do not use for collections that come from public APIs**, since they may not support random
|
||||
* access in an efficient way, and this method may actually be a lot slower. Only use for
|
||||
* collections that are created by code we control and are known to support random access.
|
||||
*/
|
||||
@OptIn(ExperimentalContracts::class)
|
||||
inline fun <T> List<T>.fastPartition(predicate: (T) -> Boolean): Pair<List<T>, List<T>> {
|
||||
contract { callsInPlace(predicate) }
|
||||
val first = ArrayList<T>()
|
||||
val second = ArrayList<T>()
|
||||
fastForEach {
|
||||
if (predicate(it)) {
|
||||
first.add(it)
|
||||
} else {
|
||||
second.add(it)
|
||||
}
|
||||
}
|
||||
return Pair(first, second)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of entries not matching the given [predicate].
|
||||
*
|
||||
* **Do not use for collections that come from public APIs**, since they may not support random
|
||||
* access in an efficient way, and this method may actually be a lot slower. Only use for
|
||||
* collections that are created by code we control and are known to support random access.
|
||||
*/
|
||||
@OptIn(ExperimentalContracts::class)
|
||||
inline fun <T> List<T>.fastCountNot(predicate: (T) -> Boolean): Int {
|
||||
contract { callsInPlace(predicate) }
|
||||
var count = size
|
||||
fastForEach { if (predicate(it)) --count }
|
||||
return count
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list containing only elements from the given collection
|
||||
* having distinct keys returned by the given [selector] function.
|
||||
*
|
||||
* Among elements of the given collection with equal keys, only the first one will be present in the resulting list.
|
||||
* The elements in the resulting list are in the same order as they were in the source collection.
|
||||
*
|
||||
* **Do not use for collections that come from public APIs**, since they may not support random
|
||||
* access in an efficient way, and this method may actually be a lot slower. Only use for
|
||||
* collections that are created by code we control and are known to support random access.
|
||||
*/
|
||||
@OptIn(ExperimentalContracts::class)
|
||||
inline fun <T, K> List<T>.fastDistinctBy(selector: (T) -> K): List<T> {
|
||||
contract { callsInPlace(selector) }
|
||||
val set = HashSet<K>()
|
||||
val list = ArrayList<T>()
|
||||
fastForEach {
|
||||
val key = selector(it)
|
||||
if (set.add(key)) list.add(it)
|
||||
}
|
||||
return list
|
||||
}
|
16
app/src/main/java/eu/kanade/core/util/DurationUtils.kt
Normal file
16
app/src/main/java/eu/kanade/core/util/DurationUtils.kt
Normal file
@ -0,0 +1,16 @@
|
||||
package eu.kanade.core.util
|
||||
|
||||
import android.content.Context
|
||||
import eu.kanade.tachiyomi.R
|
||||
import kotlin.time.Duration
|
||||
|
||||
fun Duration.toDurationString(context: Context, fallback: String): String {
|
||||
return toComponents { days, hours, minutes, seconds, _ ->
|
||||
buildList(4) {
|
||||
if (days != 0L) add(context.getString(R.string.day_short, days))
|
||||
if (hours != 0) add(context.getString(R.string.hour_short, hours))
|
||||
if (minutes != 0 && (days == 0L || hours == 0)) add(context.getString(R.string.minute_short, minutes))
|
||||
if (seconds != 0 && days == 0L && hours == 0) add(context.getString(R.string.seconds_short, seconds))
|
||||
}.joinToString(" ").ifBlank { fallback }
|
||||
}
|
||||
}
|
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,
|
||||
)
|
||||
}
|
19
app/src/main/java/eu/kanade/data/source/SourceMapper.kt
Normal file
19
app/src/main/java/eu/kanade/data/source/SourceMapper.kt
Normal file
@ -0,0 +1,19 @@
|
||||
package eu.kanade.data.source
|
||||
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import tachiyomi.domain.source.model.Source
|
||||
|
||||
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)
|
||||
}
|
@ -0,0 +1,64 @@
|
||||
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 tachiyomi.core.util.lang.awaitSingle
|
||||
import tachiyomi.core.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()
|
||||
}
|
||||
}
|
||||
|
||||
class NoResultsException : Exception()
|
@ -0,0 +1,74 @@
|
||||
package eu.kanade.data.source
|
||||
|
||||
import eu.kanade.domain.source.model.SourcePagingSourceType
|
||||
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
|
||||
import tachiyomi.data.DatabaseHandler
|
||||
import tachiyomi.domain.source.model.Source
|
||||
import tachiyomi.domain.source.model.SourceWithCount
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
147
app/src/main/java/eu/kanade/domain/DomainModule.kt
Normal file
147
app/src/main/java/eu/kanade/domain/DomainModule.kt
Normal file
@ -0,0 +1,147 @@
|
||||
package eu.kanade.domain
|
||||
|
||||
import eu.kanade.data.source.SourceRepositoryImpl
|
||||
import eu.kanade.domain.category.interactor.CreateCategoryWithName
|
||||
import eu.kanade.domain.category.interactor.DeleteCategory
|
||||
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.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.SyncChaptersWithSource
|
||||
import eu.kanade.domain.chapter.interactor.SyncChaptersWithTrackServiceTwoWay
|
||||
import eu.kanade.domain.chapter.interactor.UpdateChapter
|
||||
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.GetNextChapters
|
||||
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.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.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 tachiyomi.data.category.CategoryRepositoryImpl
|
||||
import tachiyomi.data.chapter.ChapterRepositoryImpl
|
||||
import tachiyomi.data.history.HistoryRepositoryImpl
|
||||
import tachiyomi.data.manga.MangaRepositoryImpl
|
||||
import tachiyomi.data.source.SourceDataRepositoryImpl
|
||||
import tachiyomi.data.track.TrackRepositoryImpl
|
||||
import tachiyomi.data.updates.UpdatesRepositoryImpl
|
||||
import tachiyomi.domain.category.interactor.GetCategories
|
||||
import tachiyomi.domain.category.repository.CategoryRepository
|
||||
import tachiyomi.domain.chapter.interactor.ShouldUpdateDbChapter
|
||||
import tachiyomi.domain.chapter.repository.ChapterRepository
|
||||
import tachiyomi.domain.history.interactor.GetHistory
|
||||
import tachiyomi.domain.history.interactor.GetTotalReadDuration
|
||||
import tachiyomi.domain.history.interactor.RemoveHistory
|
||||
import tachiyomi.domain.history.interactor.UpsertHistory
|
||||
import tachiyomi.domain.history.repository.HistoryRepository
|
||||
import tachiyomi.domain.manga.repository.MangaRepository
|
||||
import tachiyomi.domain.source.repository.SourceDataRepository
|
||||
import tachiyomi.domain.track.repository.TrackRepository
|
||||
import tachiyomi.domain.updates.interactor.GetUpdates
|
||||
import tachiyomi.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 { GetNextChapters(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 { GetHistory(get()) }
|
||||
addFactory { UpsertHistory(get()) }
|
||||
addFactory { RemoveHistory(get()) }
|
||||
addFactory { GetTotalReadDuration(get()) }
|
||||
|
||||
addFactory { DeleteDownload(get(), get()) }
|
||||
|
||||
addFactory { GetExtensionsByType(get(), get()) }
|
||||
addFactory { GetExtensionSources(get()) }
|
||||
addFactory { GetExtensionLanguages(get(), get()) }
|
||||
|
||||
addSingletonFactory<UpdatesRepository> { UpdatesRepositoryImpl(get()) }
|
||||
addFactory { GetUpdates(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 tachiyomi.core.preference.PreferenceStore
|
||||
import 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)
|
||||
}
|
24
app/src/main/java/eu/kanade/domain/base/BasePreferences.kt
Normal file
24
app/src/main/java/eu/kanade/domain/base/BasePreferences.kt
Normal file
@ -0,0 +1,24 @@
|
||||
package eu.kanade.domain.base
|
||||
|
||||
import android.content.Context
|
||||
import eu.kanade.tachiyomi.util.system.isPreviewBuildType
|
||||
import eu.kanade.tachiyomi.util.system.isReleaseBuildType
|
||||
import tachiyomi.core.preference.PreferenceStore
|
||||
|
||||
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() = ExtensionInstallerPreference(context, preferenceStore)
|
||||
|
||||
fun acraEnabled() = preferenceStore.getBoolean("acra.enable", isPreviewBuildType || isReleaseBuildType)
|
||||
}
|
@ -0,0 +1,68 @@
|
||||
package eu.kanade.domain.base
|
||||
|
||||
import android.content.Context
|
||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues.ExtensionInstaller
|
||||
import eu.kanade.tachiyomi.util.system.hasMiuiPackageInstaller
|
||||
import eu.kanade.tachiyomi.util.system.isShizukuInstalled
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import tachiyomi.core.preference.Preference
|
||||
import tachiyomi.core.preference.PreferenceStore
|
||||
import tachiyomi.core.preference.getEnum
|
||||
|
||||
class ExtensionInstallerPreference(
|
||||
private val context: Context,
|
||||
preferenceStore: PreferenceStore,
|
||||
) : Preference<ExtensionInstaller> {
|
||||
|
||||
private val basePref = preferenceStore.getEnum(key(), defaultValue())
|
||||
|
||||
override fun key() = "extension_installer"
|
||||
|
||||
val entries get() = ExtensionInstaller.values().run {
|
||||
if (context.hasMiuiPackageInstaller) {
|
||||
filter { it != ExtensionInstaller.PACKAGEINSTALLER }
|
||||
} else {
|
||||
toList()
|
||||
}
|
||||
}
|
||||
|
||||
override fun defaultValue() = if (context.hasMiuiPackageInstaller) {
|
||||
ExtensionInstaller.LEGACY
|
||||
} else {
|
||||
ExtensionInstaller.PACKAGEINSTALLER
|
||||
}
|
||||
|
||||
private fun check(value: ExtensionInstaller): ExtensionInstaller {
|
||||
when (value) {
|
||||
ExtensionInstaller.PACKAGEINSTALLER -> {
|
||||
if (context.hasMiuiPackageInstaller) return ExtensionInstaller.LEGACY
|
||||
}
|
||||
ExtensionInstaller.SHIZUKU -> {
|
||||
if (!context.isShizukuInstalled) return defaultValue()
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
override fun get(): ExtensionInstaller {
|
||||
val value = basePref.get()
|
||||
val checkedValue = check(value)
|
||||
if (value != checkedValue) {
|
||||
basePref.set(checkedValue)
|
||||
}
|
||||
return checkedValue
|
||||
}
|
||||
|
||||
override fun set(value: ExtensionInstaller) {
|
||||
basePref.set(check(value))
|
||||
}
|
||||
|
||||
override fun isSet() = basePref.isSet()
|
||||
|
||||
override fun delete() = basePref.delete()
|
||||
|
||||
override fun changes() = basePref.changes()
|
||||
|
||||
override fun stateIn(scope: CoroutineScope) = basePref.stateIn(scope)
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
package eu.kanade.domain.category.interactor
|
||||
|
||||
import eu.kanade.domain.library.service.LibraryPreferences
|
||||
import logcat.LogPriority
|
||||
import tachiyomi.core.util.lang.withNonCancellableContext
|
||||
import tachiyomi.core.util.system.logcat
|
||||
import tachiyomi.domain.category.model.Category
|
||||
import tachiyomi.domain.category.repository.CategoryRepository
|
||||
|
||||
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()
|
||||
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()
|
||||
data class InternalError(val error: Throwable) : Result()
|
||||
}
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
package eu.kanade.domain.category.interactor
|
||||
|
||||
import logcat.LogPriority
|
||||
import tachiyomi.core.util.lang.withNonCancellableContext
|
||||
import tachiyomi.core.util.system.logcat
|
||||
import tachiyomi.domain.category.model.CategoryUpdate
|
||||
import tachiyomi.domain.category.repository.CategoryRepository
|
||||
|
||||
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,35 @@
|
||||
package eu.kanade.domain.category.interactor
|
||||
|
||||
import logcat.LogPriority
|
||||
import tachiyomi.core.util.lang.withNonCancellableContext
|
||||
import tachiyomi.core.util.system.logcat
|
||||
import tachiyomi.domain.category.model.Category
|
||||
import tachiyomi.domain.category.model.CategoryUpdate
|
||||
import tachiyomi.domain.category.repository.CategoryRepository
|
||||
|
||||
class RenameCategory(
|
||||
private val categoryRepository: CategoryRepository,
|
||||
) {
|
||||
|
||||
suspend fun await(categoryId: Long, name: String) = withNonCancellableContext {
|
||||
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()
|
||||
data class InternalError(val error: Throwable) : Result()
|
||||
}
|
||||
}
|
@ -0,0 +1,70 @@
|
||||
package eu.kanade.domain.category.interactor
|
||||
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import logcat.LogPriority
|
||||
import tachiyomi.core.util.lang.withNonCancellableContext
|
||||
import tachiyomi.core.util.system.logcat
|
||||
import tachiyomi.domain.category.model.Category
|
||||
import tachiyomi.domain.category.model.CategoryUpdate
|
||||
import tachiyomi.domain.category.repository.CategoryRepository
|
||||
import java.util.Collections
|
||||
|
||||
class ReorderCategory(
|
||||
private val categoryRepository: CategoryRepository,
|
||||
) {
|
||||
|
||||
private val mutex = Mutex()
|
||||
|
||||
suspend fun moveUp(category: Category): Result =
|
||||
await(category, MoveTo.UP)
|
||||
|
||||
suspend fun moveDown(category: Category): Result =
|
||||
await(category, MoveTo.DOWN)
|
||||
|
||||
private suspend fun await(category: Category, moveTo: MoveTo) = withNonCancellableContext {
|
||||
mutex.withLock {
|
||||
val categories = categoryRepository.getAll()
|
||||
.filterNot(Category::isSystemCategory)
|
||||
.toMutableList()
|
||||
|
||||
val currentIndex = categories.indexOfFirst { it.id == category.id }
|
||||
if (currentIndex == -1) {
|
||||
return@withNonCancellableContext Result.Unchanged
|
||||
}
|
||||
|
||||
val newPosition = when (moveTo) {
|
||||
MoveTo.UP -> currentIndex - 1
|
||||
MoveTo.DOWN -> currentIndex + 1
|
||||
}.toInt()
|
||||
|
||||
try {
|
||||
Collections.swap(categories, currentIndex, newPosition)
|
||||
|
||||
val updates = categories.mapIndexed { index, category ->
|
||||
CategoryUpdate(
|
||||
id = category.id,
|
||||
order = index.toLong(),
|
||||
)
|
||||
}
|
||||
|
||||
categoryRepository.updatePartial(updates)
|
||||
Result.Success
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR, e)
|
||||
Result.InternalError(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed class Result {
|
||||
object Success : Result()
|
||||
object Unchanged : Result()
|
||||
data class InternalError(val error: Throwable) : Result()
|
||||
}
|
||||
|
||||
private enum class MoveTo {
|
||||
UP,
|
||||
DOWN,
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
package eu.kanade.domain.category.interactor
|
||||
|
||||
import eu.kanade.domain.library.service.LibraryPreferences
|
||||
import tachiyomi.domain.category.repository.CategoryRepository
|
||||
import tachiyomi.domain.library.model.plus
|
||||
|
||||
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.library.service.LibraryPreferences
|
||||
import tachiyomi.domain.category.model.Category
|
||||
import tachiyomi.domain.category.model.CategoryUpdate
|
||||
import tachiyomi.domain.category.repository.CategoryRepository
|
||||
import tachiyomi.domain.library.model.LibraryDisplayMode
|
||||
import tachiyomi.domain.library.model.plus
|
||||
|
||||
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 logcat.LogPriority
|
||||
import tachiyomi.core.util.system.logcat
|
||||
import tachiyomi.domain.manga.repository.MangaRepository
|
||||
|
||||
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.library.service.LibraryPreferences
|
||||
import tachiyomi.domain.category.model.Category
|
||||
import tachiyomi.domain.category.model.CategoryUpdate
|
||||
import tachiyomi.domain.category.repository.CategoryRepository
|
||||
import tachiyomi.domain.library.model.LibrarySort
|
||||
import tachiyomi.domain.library.model.plus
|
||||
|
||||
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 tachiyomi.core.util.lang.withNonCancellableContext
|
||||
import tachiyomi.domain.category.model.CategoryUpdate
|
||||
import tachiyomi.domain.category.repository.CategoryRepository
|
||||
|
||||
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,29 @@
|
||||
package eu.kanade.domain.chapter.interactor
|
||||
|
||||
import logcat.LogPriority
|
||||
import tachiyomi.core.util.system.logcat
|
||||
import tachiyomi.domain.chapter.model.Chapter
|
||||
import tachiyomi.domain.chapter.repository.ChapterRepository
|
||||
|
||||
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 logcat.LogPriority
|
||||
import tachiyomi.core.util.system.logcat
|
||||
import tachiyomi.domain.chapter.model.Chapter
|
||||
import tachiyomi.domain.chapter.repository.ChapterRepository
|
||||
|
||||
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 tachiyomi.core.util.lang.withNonCancellableContext
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
|
||||
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.download.interactor.DeleteDownload
|
||||
import eu.kanade.domain.download.service.DownloadPreferences
|
||||
import logcat.LogPriority
|
||||
import tachiyomi.core.util.lang.withNonCancellableContext
|
||||
import tachiyomi.core.util.system.logcat
|
||||
import tachiyomi.domain.chapter.model.Chapter
|
||||
import tachiyomi.domain.chapter.model.ChapterUpdate
|
||||
import tachiyomi.domain.chapter.repository.ChapterRepository
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.domain.manga.repository.MangaRepository
|
||||
|
||||
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,198 @@
|
||||
package eu.kanade.domain.chapter.interactor
|
||||
|
||||
import eu.kanade.domain.chapter.model.copyFromSChapter
|
||||
import eu.kanade.domain.chapter.model.toSChapter
|
||||
import eu.kanade.domain.manga.interactor.UpdateManga
|
||||
import eu.kanade.domain.manga.model.toSManga
|
||||
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 tachiyomi.data.chapter.ChapterSanitizer
|
||||
import tachiyomi.domain.chapter.interactor.ShouldUpdateDbChapter
|
||||
import tachiyomi.domain.chapter.model.Chapter
|
||||
import tachiyomi.domain.chapter.model.NoChaptersException
|
||||
import tachiyomi.domain.chapter.model.toChapterUpdate
|
||||
import tachiyomi.domain.chapter.repository.ChapterRepository
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
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 = with(ChapterSanitizer) { sChapter.name.sanitize(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, chapter)
|
||||
}
|
||||
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.track.interactor.InsertTrack
|
||||
import eu.kanade.domain.track.model.toDbTrack
|
||||
import eu.kanade.tachiyomi.data.track.TrackService
|
||||
import logcat.LogPriority
|
||||
import tachiyomi.core.util.system.logcat
|
||||
import tachiyomi.domain.chapter.model.Chapter
|
||||
import tachiyomi.domain.chapter.model.toChapterUpdate
|
||||
import tachiyomi.domain.track.model.Track
|
||||
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 logcat.LogPriority
|
||||
import tachiyomi.core.util.system.logcat
|
||||
import tachiyomi.domain.chapter.model.ChapterUpdate
|
||||
import tachiyomi.domain.chapter.repository.ChapterRepository
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
42
app/src/main/java/eu/kanade/domain/chapter/model/Chapter.kt
Normal file
42
app/src/main/java/eu/kanade/domain/chapter/model/Chapter.kt
Normal file
@ -0,0 +1,42 @@
|
||||
package eu.kanade.domain.chapter.model
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.models.ChapterImpl
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import tachiyomi.domain.chapter.model.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter as DbChapter
|
||||
|
||||
// TODO: Remove when all deps are migrated
|
||||
fun Chapter.toSChapter(): SChapter {
|
||||
return SChapter.create().also {
|
||||
it.url = url
|
||||
it.name = name
|
||||
it.date_upload = dateUpload
|
||||
it.chapter_number = chapterNumber
|
||||
it.scanlator = scanlator
|
||||
}
|
||||
}
|
||||
|
||||
fun Chapter.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 },
|
||||
)
|
||||
}
|
||||
|
||||
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,84 @@
|
||||
package eu.kanade.domain.chapter.model
|
||||
|
||||
import eu.kanade.domain.manga.model.downloadedFilter
|
||||
import eu.kanade.domain.manga.model.isLocal
|
||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||
import eu.kanade.tachiyomi.data.download.model.Download
|
||||
import eu.kanade.tachiyomi.ui.manga.ChapterItem
|
||||
import eu.kanade.tachiyomi.util.chapter.getChapterSort
|
||||
import tachiyomi.domain.chapter.model.Chapter
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.domain.manga.model.TriStateFilter
|
||||
|
||||
/**
|
||||
* Applies the view filters to the list of chapters obtained from the database.
|
||||
* @return an observable of the list of chapters filtered and sorted.
|
||||
*/
|
||||
fun List<Chapter>.applyFilters(manga: Manga, downloadManager: DownloadManager): List<Chapter> {
|
||||
val isLocalManga = manga.isLocal()
|
||||
val unreadFilter = manga.unreadFilter
|
||||
val downloadedFilter = manga.downloadedFilter
|
||||
val bookmarkedFilter = manga.bookmarkedFilter
|
||||
|
||||
return filter { chapter ->
|
||||
when (unreadFilter) {
|
||||
TriStateFilter.DISABLED -> true
|
||||
TriStateFilter.ENABLED_IS -> !chapter.read
|
||||
TriStateFilter.ENABLED_NOT -> chapter.read
|
||||
}
|
||||
}
|
||||
.filter { chapter ->
|
||||
when (bookmarkedFilter) {
|
||||
TriStateFilter.DISABLED -> true
|
||||
TriStateFilter.ENABLED_IS -> chapter.bookmark
|
||||
TriStateFilter.ENABLED_NOT -> !chapter.bookmark
|
||||
}
|
||||
}
|
||||
.filter { chapter ->
|
||||
val downloaded = downloadManager.isChapterDownloaded(chapter.name, chapter.scanlator, manga.title, manga.source)
|
||||
val downloadState = when {
|
||||
downloaded -> Download.State.DOWNLOADED
|
||||
else -> Download.State.NOT_DOWNLOADED
|
||||
}
|
||||
when (downloadedFilter) {
|
||||
TriStateFilter.DISABLED -> true
|
||||
TriStateFilter.ENABLED_IS -> downloadState == Download.State.DOWNLOADED || isLocalManga
|
||||
TriStateFilter.ENABLED_NOT -> downloadState != Download.State.DOWNLOADED && !isLocalManga
|
||||
}
|
||||
}
|
||||
.sortedWith(getChapterSort(manga))
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the view filters to the list of chapters obtained from the database.
|
||||
* @return an observable of the list of chapters filtered and sorted.
|
||||
*/
|
||||
fun List<ChapterItem>.applyFilters(manga: Manga): Sequence<ChapterItem> {
|
||||
val isLocalManga = manga.isLocal()
|
||||
val unreadFilter = manga.unreadFilter
|
||||
val downloadedFilter = manga.downloadedFilter
|
||||
val bookmarkedFilter = manga.bookmarkedFilter
|
||||
return asSequence()
|
||||
.filter { (chapter) ->
|
||||
when (unreadFilter) {
|
||||
TriStateFilter.DISABLED -> true
|
||||
TriStateFilter.ENABLED_IS -> !chapter.read
|
||||
TriStateFilter.ENABLED_NOT -> chapter.read
|
||||
}
|
||||
}
|
||||
.filter { (chapter) ->
|
||||
when (bookmarkedFilter) {
|
||||
TriStateFilter.DISABLED -> true
|
||||
TriStateFilter.ENABLED_IS -> chapter.bookmark
|
||||
TriStateFilter.ENABLED_NOT -> !chapter.bookmark
|
||||
}
|
||||
}
|
||||
.filter {
|
||||
when (downloadedFilter) {
|
||||
TriStateFilter.DISABLED -> true
|
||||
TriStateFilter.ENABLED_IS -> it.isDownloaded || isLocalManga
|
||||
TriStateFilter.ENABLED_NOT -> !it.isDownloaded && !isLocalManga
|
||||
}
|
||||
}
|
||||
.sortedWith { (chapter1), (chapter2) -> getChapterSort(manga).invoke(chapter1, chapter2) }
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
package eu.kanade.domain.download.interactor
|
||||
|
||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import tachiyomi.core.util.lang.withNonCancellableContext
|
||||
import tachiyomi.domain.chapter.model.Chapter
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
|
||||
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.toList(), manga, source)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
package eu.kanade.domain.download.service
|
||||
|
||||
import tachiyomi.core.preference.PreferenceStore
|
||||
import 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,32 @@
|
||||
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<String> { it !in enabledLanguage }.then(LocaleHelper.comparator),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
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 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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class ExtensionSourceItem(
|
||||
val source: Source,
|
||||
val enabled: Boolean,
|
||||
val labelAsName: Boolean,
|
||||
)
|
@ -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,52 @@
|
||||
package eu.kanade.domain.history.interactor
|
||||
|
||||
import eu.kanade.domain.chapter.interactor.GetChapterByMangaId
|
||||
import eu.kanade.domain.manga.interactor.GetManga
|
||||
import eu.kanade.tachiyomi.util.chapter.getChapterSort
|
||||
import tachiyomi.domain.chapter.model.Chapter
|
||||
import tachiyomi.domain.history.repository.HistoryRepository
|
||||
import kotlin.math.max
|
||||
|
||||
class GetNextChapters(
|
||||
private val getChapterByMangaId: GetChapterByMangaId,
|
||||
private val getManga: GetManga,
|
||||
private val historyRepository: HistoryRepository,
|
||||
) {
|
||||
|
||||
suspend fun await(onlyUnread: Boolean = true): List<Chapter> {
|
||||
val history = historyRepository.getLastHistory() ?: return emptyList()
|
||||
return await(history.mangaId, history.chapterId, onlyUnread)
|
||||
}
|
||||
|
||||
suspend fun await(mangaId: Long, onlyUnread: Boolean = true): List<Chapter> {
|
||||
val manga = getManga.await(mangaId) ?: return emptyList()
|
||||
val chapters = getChapterByMangaId.await(mangaId)
|
||||
.sortedWith(getChapterSort(manga, sortDescending = false))
|
||||
|
||||
return if (onlyUnread) {
|
||||
chapters.filterNot { it.read }
|
||||
} else {
|
||||
chapters
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun await(mangaId: Long, fromChapterId: Long, onlyUnread: Boolean = true): List<Chapter> {
|
||||
val chapters = await(mangaId, onlyUnread)
|
||||
val currChapterIndex = chapters.indexOfFirst { it.id == fromChapterId }
|
||||
val nextChapters = chapters.subList(max(0, currChapterIndex), chapters.size)
|
||||
|
||||
if (onlyUnread) {
|
||||
return nextChapters
|
||||
}
|
||||
|
||||
// The "next chapter" is either:
|
||||
// - The current chapter if it isn't completely read
|
||||
// - The chapters after the current chapter if the current one is completely read
|
||||
val fromChapter = chapters.getOrNull(currChapterIndex)
|
||||
return if (fromChapter != null && !fromChapter.read) {
|
||||
nextChapters
|
||||
} else {
|
||||
nextChapters.drop(1)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,111 @@
|
||||
package eu.kanade.domain.library.service
|
||||
|
||||
import eu.kanade.tachiyomi.data.preference.DEVICE_ONLY_ON_WIFI
|
||||
import eu.kanade.tachiyomi.data.preference.MANGA_HAS_UNREAD
|
||||
import eu.kanade.tachiyomi.data.preference.MANGA_NON_COMPLETED
|
||||
import eu.kanade.tachiyomi.data.preference.MANGA_NON_READ
|
||||
import eu.kanade.tachiyomi.widget.ExtendedNavigationView
|
||||
import tachiyomi.core.preference.PreferenceStore
|
||||
import tachiyomi.domain.library.model.LibraryDisplayMode
|
||||
import tachiyomi.domain.library.model.LibrarySort
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
|
||||
class LibraryPreferences(
|
||||
private val preferenceStore: PreferenceStore,
|
||||
) {
|
||||
|
||||
fun libraryDisplayMode() = preferenceStore.getObject("pref_display_mode_library", LibraryDisplayMode.default, LibraryDisplayMode.Serializer::serialize, LibraryDisplayMode.Serializer::deserialize)
|
||||
|
||||
fun librarySortingMode() = preferenceStore.getObject("library_sorting_mode", LibrarySort.default, LibrarySort.Serializer::serialize, LibrarySort.Serializer::deserialize)
|
||||
|
||||
fun portraitColumns() = preferenceStore.getInt("pref_library_columns_portrait_key", 0)
|
||||
|
||||
fun landscapeColumns() = preferenceStore.getInt("pref_library_columns_landscape_key", 0)
|
||||
|
||||
fun libraryUpdateInterval() = preferenceStore.getInt("pref_library_update_interval_key", 0)
|
||||
fun libraryUpdateLastTimestamp() = preferenceStore.getLong("library_update_last_timestamp", 0L)
|
||||
|
||||
fun libraryUpdateDeviceRestriction() = preferenceStore.getStringSet("library_update_restriction", setOf(DEVICE_ONLY_ON_WIFI))
|
||||
fun libraryUpdateMangaRestriction() = preferenceStore.getStringSet("library_update_manga_restriction", setOf(MANGA_HAS_UNREAD, MANGA_NON_COMPLETED, MANGA_NON_READ))
|
||||
|
||||
fun autoUpdateMetadata() = preferenceStore.getBoolean("auto_update_metadata", false)
|
||||
|
||||
fun autoUpdateTrackers() = preferenceStore.getBoolean("auto_update_trackers", false)
|
||||
|
||||
fun showContinueReadingButton() = preferenceStore.getBoolean("display_continue_reading_button", false)
|
||||
|
||||
// region Filter
|
||||
|
||||
fun filterDownloaded() = preferenceStore.getInt("pref_filter_library_downloaded", ExtendedNavigationView.Item.TriStateGroup.State.IGNORE.value)
|
||||
|
||||
fun filterUnread() = preferenceStore.getInt("pref_filter_library_unread", ExtendedNavigationView.Item.TriStateGroup.State.IGNORE.value)
|
||||
|
||||
fun filterStarted() = preferenceStore.getInt("pref_filter_library_started", ExtendedNavigationView.Item.TriStateGroup.State.IGNORE.value)
|
||||
|
||||
fun filterBookmarked() = preferenceStore.getInt("pref_filter_library_bookmarked", ExtendedNavigationView.Item.TriStateGroup.State.IGNORE.value)
|
||||
|
||||
fun filterCompleted() = preferenceStore.getInt("pref_filter_library_completed", ExtendedNavigationView.Item.TriStateGroup.State.IGNORE.value)
|
||||
|
||||
fun filterTracking(name: Int) = preferenceStore.getInt("pref_filter_library_tracked_$name", ExtendedNavigationView.Item.TriStateGroup.State.IGNORE.value)
|
||||
|
||||
// endregion
|
||||
|
||||
// region Badges
|
||||
|
||||
fun downloadBadge() = preferenceStore.getBoolean("display_download_badge", false)
|
||||
|
||||
fun localBadge() = preferenceStore.getBoolean("display_local_badge", true)
|
||||
|
||||
fun languageBadge() = preferenceStore.getBoolean("display_language_badge", false)
|
||||
|
||||
fun newShowUpdatesCount() = preferenceStore.getBoolean("library_show_updates_count", true)
|
||||
fun newUpdatesCount() = preferenceStore.getInt("library_unseen_updates_count", 0)
|
||||
|
||||
// endregion
|
||||
|
||||
// region Category
|
||||
|
||||
fun defaultCategory() = preferenceStore.getInt("default_category", -1)
|
||||
|
||||
fun lastUsedCategory() = preferenceStore.getInt("last_used_category", 0)
|
||||
|
||||
fun categoryTabs() = preferenceStore.getBoolean("display_category_tabs", true)
|
||||
|
||||
fun categoryNumberOfItems() = preferenceStore.getBoolean("display_number_of_items", false)
|
||||
|
||||
fun categorizedDisplaySettings() = preferenceStore.getBoolean("categorized_display", false)
|
||||
|
||||
fun libraryUpdateCategories() = preferenceStore.getStringSet("library_update_categories", emptySet())
|
||||
|
||||
fun libraryUpdateCategoriesExclude() = preferenceStore.getStringSet("library_update_categories_exclude", emptySet())
|
||||
|
||||
// endregion
|
||||
|
||||
// region Chapter
|
||||
|
||||
fun filterChapterByRead() = preferenceStore.getLong("default_chapter_filter_by_read", Manga.SHOW_ALL)
|
||||
|
||||
fun filterChapterByDownloaded() = preferenceStore.getLong("default_chapter_filter_by_downloaded", Manga.SHOW_ALL)
|
||||
|
||||
fun filterChapterByBookmarked() = preferenceStore.getLong("default_chapter_filter_by_bookmarked", Manga.SHOW_ALL)
|
||||
|
||||
// and upload date
|
||||
fun sortChapterBySourceOrNumber() = preferenceStore.getLong("default_chapter_sort_by_source_or_number", Manga.CHAPTER_SORTING_SOURCE)
|
||||
|
||||
fun displayChapterByNameOrNumber() = preferenceStore.getLong("default_chapter_display_by_name_or_number", Manga.CHAPTER_DISPLAY_NAME)
|
||||
|
||||
fun sortChapterByAscendingOrDescending() = preferenceStore.getLong("default_chapter_sort_by_ascending_or_descending", Manga.CHAPTER_SORT_DESC)
|
||||
|
||||
fun setChapterSettingsDefault(manga: Manga) {
|
||||
filterChapterByRead().set(manga.unreadFilterRaw)
|
||||
filterChapterByDownloaded().set(manga.downloadedFilterRaw)
|
||||
filterChapterByBookmarked().set(manga.bookmarkedFilterRaw)
|
||||
sortChapterBySourceOrNumber().set(manga.sorting)
|
||||
displayChapterByNameOrNumber().set(manga.displayMode)
|
||||
sortChapterByAscendingOrDescending().set(if (manga.sortDescending()) Manga.CHAPTER_SORT_DESC else Manga.CHAPTER_SORT_ASC)
|
||||
}
|
||||
|
||||
fun autoClearChapterCache() = preferenceStore.getBoolean("auto_clear_chapter_cache", false)
|
||||
|
||||
// endregion
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
package eu.kanade.domain.manga.interactor
|
||||
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.domain.manga.repository.MangaRepository
|
||||
|
||||
class GetDuplicateLibraryManga(
|
||||
private val mangaRepository: MangaRepository,
|
||||
) {
|
||||
|
||||
suspend fun await(title: String): Manga? {
|
||||
return mangaRepository.getDuplicateLibraryManga(title.lowercase())
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
package eu.kanade.domain.manga.interactor
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.domain.manga.repository.MangaRepository
|
||||
|
||||
class GetFavorites(
|
||||
private val mangaRepository: MangaRepository,
|
||||
) {
|
||||
|
||||
suspend fun await(): List<Manga> {
|
||||
return mangaRepository.getFavorites()
|
||||
}
|
||||
|
||||
fun subscribe(sourceId: Long): Flow<List<Manga>> {
|
||||
return mangaRepository.getFavoritesBySourceId(sourceId)
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
package eu.kanade.domain.manga.interactor
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import tachiyomi.domain.library.model.LibraryManga
|
||||
import tachiyomi.domain.manga.repository.MangaRepository
|
||||
|
||||
class GetLibraryManga(
|
||||
private val mangaRepository: MangaRepository,
|
||||
) {
|
||||
|
||||
suspend fun await(): List<LibraryManga> {
|
||||
return mangaRepository.getLibraryManga()
|
||||
}
|
||||
|
||||
fun subscribe(): Flow<List<LibraryManga>> {
|
||||
return mangaRepository.getLibraryMangaAsFlow()
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
package eu.kanade.domain.manga.interactor
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import logcat.LogPriority
|
||||
import tachiyomi.core.util.system.logcat
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.domain.manga.repository.MangaRepository
|
||||
|
||||
class GetManga(
|
||||
private val mangaRepository: MangaRepository,
|
||||
) {
|
||||
|
||||
suspend fun await(id: Long): Manga? {
|
||||
return try {
|
||||
mangaRepository.getMangaById(id)
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR, e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun subscribe(id: Long): Flow<Manga> {
|
||||
return mangaRepository.getMangaByIdAsFlow(id)
|
||||
}
|
||||
|
||||
fun subscribe(url: String, sourceId: Long): Flow<Manga?> {
|
||||
return mangaRepository.getMangaByUrlAndSourceIdAsFlow(url, sourceId)
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
package eu.kanade.domain.manga.interactor
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import tachiyomi.domain.chapter.model.Chapter
|
||||
import tachiyomi.domain.chapter.repository.ChapterRepository
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.domain.manga.repository.MangaRepository
|
||||
|
||||
class GetMangaWithChapters(
|
||||
private val mangaRepository: MangaRepository,
|
||||
private val chapterRepository: ChapterRepository,
|
||||
) {
|
||||
|
||||
suspend fun subscribe(id: Long): Flow<Pair<Manga, List<Chapter>>> {
|
||||
return combine(
|
||||
mangaRepository.getMangaByIdAsFlow(id),
|
||||
chapterRepository.getChapterByMangaIdAsFlow(id),
|
||||
) { manga, chapters ->
|
||||
Pair(manga, chapters)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun awaitManga(id: Long): Manga {
|
||||
return mangaRepository.getMangaById(id)
|
||||
}
|
||||
|
||||
suspend fun awaitChapters(id: Long): List<Chapter> {
|
||||
return chapterRepository.getChapterByMangaId(id)
|
||||
}
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
package eu.kanade.domain.manga.interactor
|
||||
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.domain.manga.repository.MangaRepository
|
||||
|
||||
class NetworkToLocalManga(
|
||||
private val mangaRepository: MangaRepository,
|
||||
) {
|
||||
|
||||
suspend fun await(manga: Manga): Manga {
|
||||
val localManga = getManga(manga.url, manga.source)
|
||||
return when {
|
||||
localManga == null -> {
|
||||
val id = insertManga(manga)
|
||||
manga.copy(id = id!!)
|
||||
}
|
||||
!localManga.favorite -> {
|
||||
// if the manga isn't a favorite, set its display title from source
|
||||
// if it later becomes a favorite, updated title will go to db
|
||||
localManga.copy(title = manga.title)
|
||||
}
|
||||
else -> {
|
||||
localManga
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getManga(url: String, sourceId: Long): Manga? {
|
||||
return mangaRepository.getMangaByUrlAndSourceId(url, sourceId)
|
||||
}
|
||||
|
||||
private suspend fun insertManga(manga: Manga): Long? {
|
||||
return mangaRepository.insert(manga)
|
||||
}
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
package eu.kanade.domain.manga.interactor
|
||||
|
||||
import tachiyomi.domain.manga.repository.MangaRepository
|
||||
|
||||
class ResetViewerFlags(
|
||||
private val mangaRepository: MangaRepository,
|
||||
) {
|
||||
|
||||
suspend fun await(): Boolean {
|
||||
return mangaRepository.resetViewerFlags()
|
||||
}
|
||||
}
|
@ -0,0 +1,97 @@
|
||||
package eu.kanade.domain.manga.interactor
|
||||
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.domain.manga.model.MangaUpdate
|
||||
import tachiyomi.domain.manga.repository.MangaRepository
|
||||
|
||||
class SetMangaChapterFlags(
|
||||
private val mangaRepository: MangaRepository,
|
||||
) {
|
||||
|
||||
suspend fun awaitSetDownloadedFilter(manga: Manga, flag: Long): Boolean {
|
||||
return mangaRepository.update(
|
||||
MangaUpdate(
|
||||
id = manga.id,
|
||||
chapterFlags = manga.chapterFlags.setFlag(flag, Manga.CHAPTER_DOWNLOADED_MASK),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun awaitSetUnreadFilter(manga: Manga, flag: Long): Boolean {
|
||||
return mangaRepository.update(
|
||||
MangaUpdate(
|
||||
id = manga.id,
|
||||
chapterFlags = manga.chapterFlags.setFlag(flag, Manga.CHAPTER_UNREAD_MASK),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun awaitSetBookmarkFilter(manga: Manga, flag: Long): Boolean {
|
||||
return mangaRepository.update(
|
||||
MangaUpdate(
|
||||
id = manga.id,
|
||||
chapterFlags = manga.chapterFlags.setFlag(flag, Manga.CHAPTER_BOOKMARKED_MASK),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun awaitSetDisplayMode(manga: Manga, flag: Long): Boolean {
|
||||
return mangaRepository.update(
|
||||
MangaUpdate(
|
||||
id = manga.id,
|
||||
chapterFlags = manga.chapterFlags.setFlag(flag, Manga.CHAPTER_DISPLAY_MASK),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun awaitSetSortingModeOrFlipOrder(manga: Manga, flag: Long): Boolean {
|
||||
val newFlags = manga.chapterFlags.let {
|
||||
if (manga.sorting == flag) {
|
||||
// Just flip the order
|
||||
val orderFlag = if (manga.sortDescending()) {
|
||||
Manga.CHAPTER_SORT_ASC
|
||||
} else {
|
||||
Manga.CHAPTER_SORT_DESC
|
||||
}
|
||||
it.setFlag(orderFlag, Manga.CHAPTER_SORT_DIR_MASK)
|
||||
} else {
|
||||
// Set new flag with ascending order
|
||||
it
|
||||
.setFlag(flag, Manga.CHAPTER_SORTING_MASK)
|
||||
.setFlag(Manga.CHAPTER_SORT_ASC, Manga.CHAPTER_SORT_DIR_MASK)
|
||||
}
|
||||
}
|
||||
return mangaRepository.update(
|
||||
MangaUpdate(
|
||||
id = manga.id,
|
||||
chapterFlags = newFlags,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun awaitSetAllFlags(
|
||||
mangaId: Long,
|
||||
unreadFilter: Long,
|
||||
downloadedFilter: Long,
|
||||
bookmarkedFilter: Long,
|
||||
sortingMode: Long,
|
||||
sortingDirection: Long,
|
||||
displayMode: Long,
|
||||
): Boolean {
|
||||
return mangaRepository.update(
|
||||
MangaUpdate(
|
||||
id = mangaId,
|
||||
chapterFlags = 0L.setFlag(unreadFilter, Manga.CHAPTER_UNREAD_MASK)
|
||||
.setFlag(downloadedFilter, Manga.CHAPTER_DOWNLOADED_MASK)
|
||||
.setFlag(bookmarkedFilter, Manga.CHAPTER_BOOKMARKED_MASK)
|
||||
.setFlag(sortingMode, Manga.CHAPTER_SORTING_MASK)
|
||||
.setFlag(sortingDirection, Manga.CHAPTER_SORT_DIR_MASK)
|
||||
.setFlag(displayMode, Manga.CHAPTER_DISPLAY_MASK),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun Long.setFlag(flag: Long, mask: Long): Long {
|
||||
return this and mask.inv() or (flag and mask)
|
||||
}
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
package eu.kanade.domain.manga.interactor
|
||||
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
|
||||
import tachiyomi.domain.manga.model.MangaUpdate
|
||||
import tachiyomi.domain.manga.repository.MangaRepository
|
||||
|
||||
class SetMangaViewerFlags(
|
||||
private val mangaRepository: MangaRepository,
|
||||
) {
|
||||
|
||||
suspend fun awaitSetMangaReadingMode(id: Long, flag: Long) {
|
||||
val manga = mangaRepository.getMangaById(id)
|
||||
mangaRepository.update(
|
||||
MangaUpdate(
|
||||
id = id,
|
||||
viewerFlags = manga.viewerFlags.setFlag(flag, ReadingModeType.MASK.toLong()),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun awaitSetOrientationType(id: Long, flag: Long) {
|
||||
val manga = mangaRepository.getMangaById(id)
|
||||
mangaRepository.update(
|
||||
MangaUpdate(
|
||||
id = id,
|
||||
viewerFlags = manga.viewerFlags.setFlag(flag, OrientationType.MASK.toLong()),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun Long.setFlag(flag: Long, mask: Long): Long {
|
||||
return this and mask.inv() or (flag and mask)
|
||||
}
|
||||
}
|
@ -0,0 +1,93 @@
|
||||
package eu.kanade.domain.manga.interactor
|
||||
|
||||
import eu.kanade.domain.manga.model.hasCustomCover
|
||||
import eu.kanade.domain.manga.model.isLocal
|
||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.domain.manga.model.MangaUpdate
|
||||
import tachiyomi.domain.manga.repository.MangaRepository
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.util.Date
|
||||
|
||||
class UpdateManga(
|
||||
private val mangaRepository: MangaRepository,
|
||||
) {
|
||||
|
||||
suspend fun await(mangaUpdate: MangaUpdate): Boolean {
|
||||
return mangaRepository.update(mangaUpdate)
|
||||
}
|
||||
|
||||
suspend fun awaitAll(mangaUpdates: List<MangaUpdate>): Boolean {
|
||||
return mangaRepository.updateAll(mangaUpdates)
|
||||
}
|
||||
|
||||
suspend fun awaitUpdateFromSource(
|
||||
localManga: Manga,
|
||||
remoteManga: SManga,
|
||||
manualFetch: Boolean,
|
||||
coverCache: CoverCache = Injekt.get(),
|
||||
): Boolean {
|
||||
val remoteTitle = try {
|
||||
remoteManga.title
|
||||
} catch (_: UninitializedPropertyAccessException) {
|
||||
""
|
||||
}
|
||||
|
||||
// if the manga isn't a favorite, set its title from source and update in db
|
||||
val title = if (remoteTitle.isEmpty() || localManga.favorite) null else remoteTitle
|
||||
|
||||
val coverLastModified =
|
||||
when {
|
||||
// Never refresh covers if the url is empty to avoid "losing" existing covers
|
||||
remoteManga.thumbnail_url.isNullOrEmpty() -> null
|
||||
!manualFetch && localManga.thumbnailUrl == remoteManga.thumbnail_url -> null
|
||||
localManga.isLocal() -> Date().time
|
||||
localManga.hasCustomCover(coverCache) -> {
|
||||
coverCache.deleteFromCache(localManga, false)
|
||||
null
|
||||
}
|
||||
else -> {
|
||||
coverCache.deleteFromCache(localManga, false)
|
||||
Date().time
|
||||
}
|
||||
}
|
||||
|
||||
val thumbnailUrl = remoteManga.thumbnail_url?.takeIf { it.isNotEmpty() }
|
||||
|
||||
return mangaRepository.update(
|
||||
MangaUpdate(
|
||||
id = localManga.id,
|
||||
title = title,
|
||||
coverLastModified = coverLastModified,
|
||||
author = remoteManga.author,
|
||||
artist = remoteManga.artist,
|
||||
description = remoteManga.description,
|
||||
genre = remoteManga.getGenres(),
|
||||
thumbnailUrl = thumbnailUrl,
|
||||
status = remoteManga.status.toLong(),
|
||||
updateStrategy = remoteManga.update_strategy,
|
||||
initialized = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun awaitUpdateLastUpdate(mangaId: Long): Boolean {
|
||||
return mangaRepository.update(MangaUpdate(id = mangaId, lastUpdate = Date().time))
|
||||
}
|
||||
|
||||
suspend fun awaitUpdateCoverLastModified(mangaId: Long): Boolean {
|
||||
return mangaRepository.update(MangaUpdate(id = mangaId, coverLastModified = Date().time))
|
||||
}
|
||||
|
||||
suspend fun awaitUpdateFavorite(mangaId: Long, favorite: Boolean): Boolean {
|
||||
val dateAdded = when (favorite) {
|
||||
true -> Date().time
|
||||
false -> 0
|
||||
}
|
||||
return mangaRepository.update(
|
||||
MangaUpdate(id = mangaId, favorite = favorite, dateAdded = dateAdded),
|
||||
)
|
||||
}
|
||||
}
|
176
app/src/main/java/eu/kanade/domain/manga/model/ComicInfo.kt
Normal file
176
app/src/main/java/eu/kanade/domain/manga/model/ComicInfo.kt
Normal file
@ -0,0 +1,176 @@
|
||||
package eu.kanade.domain.manga.model
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import kotlinx.serialization.Serializable
|
||||
import nl.adaptivity.xmlutil.serialization.XmlElement
|
||||
import nl.adaptivity.xmlutil.serialization.XmlSerialName
|
||||
import nl.adaptivity.xmlutil.serialization.XmlValue
|
||||
import tachiyomi.domain.chapter.model.Chapter
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
|
||||
const val COMIC_INFO_FILE = "ComicInfo.xml"
|
||||
|
||||
/**
|
||||
* Creates a ComicInfo instance based on the manga and chapter metadata.
|
||||
*/
|
||||
fun getComicInfo(manga: Manga, chapter: Chapter, chapterUrl: String) = ComicInfo(
|
||||
title = ComicInfo.Title(chapter.name),
|
||||
series = ComicInfo.Series(manga.title),
|
||||
web = ComicInfo.Web(chapterUrl),
|
||||
summary = manga.description?.let { ComicInfo.Summary(it) },
|
||||
writer = manga.author?.let { ComicInfo.Writer(it) },
|
||||
penciller = manga.artist?.let { ComicInfo.Penciller(it) },
|
||||
translator = chapter.scanlator?.let { ComicInfo.Translator(it) },
|
||||
genre = manga.genre?.let { ComicInfo.Genre(it.joinToString()) },
|
||||
publishingStatus = ComicInfo.PublishingStatusTachiyomi(
|
||||
ComicInfoPublishingStatus.toComicInfoValue(manga.status),
|
||||
),
|
||||
inker = null,
|
||||
colorist = null,
|
||||
letterer = null,
|
||||
coverArtist = null,
|
||||
tags = null,
|
||||
)
|
||||
|
||||
fun SManga.copyFromComicInfo(comicInfo: ComicInfo) {
|
||||
comicInfo.series?.let { title = it.value }
|
||||
comicInfo.writer?.let { author = it.value }
|
||||
comicInfo.summary?.let { description = it.value }
|
||||
|
||||
listOfNotNull(
|
||||
comicInfo.genre?.value,
|
||||
comicInfo.tags?.value,
|
||||
)
|
||||
.flatMap { it.split(", ") }
|
||||
.distinct()
|
||||
.joinToString(", ") { it.trim() }
|
||||
.takeIf { it.isNotEmpty() }
|
||||
?.let { genre = it }
|
||||
|
||||
listOfNotNull(
|
||||
comicInfo.penciller?.value,
|
||||
comicInfo.inker?.value,
|
||||
comicInfo.colorist?.value,
|
||||
comicInfo.letterer?.value,
|
||||
comicInfo.coverArtist?.value,
|
||||
)
|
||||
.flatMap { it.split(", ") }
|
||||
.distinct()
|
||||
.joinToString(", ") { it.trim() }
|
||||
.takeIf { it.isNotEmpty() }
|
||||
?.let { artist = it }
|
||||
|
||||
status = ComicInfoPublishingStatus.toSMangaValue(comicInfo.publishingStatus?.value)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
@XmlSerialName("ComicInfo", "", "")
|
||||
data class ComicInfo(
|
||||
val title: Title?,
|
||||
val series: Series?,
|
||||
val summary: Summary?,
|
||||
val writer: Writer?,
|
||||
val penciller: Penciller?,
|
||||
val inker: Inker?,
|
||||
val colorist: Colorist?,
|
||||
val letterer: Letterer?,
|
||||
val coverArtist: CoverArtist?,
|
||||
val translator: Translator?,
|
||||
val genre: Genre?,
|
||||
val tags: Tags?,
|
||||
val web: Web?,
|
||||
val publishingStatus: PublishingStatusTachiyomi?,
|
||||
) {
|
||||
@Suppress("UNUSED")
|
||||
@XmlElement(false)
|
||||
@XmlSerialName("xmlns:xsd", "", "")
|
||||
val xmlSchema: String = "http://www.w3.org/2001/XMLSchema"
|
||||
|
||||
@Suppress("UNUSED")
|
||||
@XmlElement(false)
|
||||
@XmlSerialName("xmlns:xsi", "", "")
|
||||
val xmlSchemaInstance: String = "http://www.w3.org/2001/XMLSchema-instance"
|
||||
|
||||
@Serializable
|
||||
@XmlSerialName("Title", "", "")
|
||||
data class Title(@XmlValue(true) val value: String = "")
|
||||
|
||||
@Serializable
|
||||
@XmlSerialName("Series", "", "")
|
||||
data class Series(@XmlValue(true) val value: String = "")
|
||||
|
||||
@Serializable
|
||||
@XmlSerialName("Summary", "", "")
|
||||
data class Summary(@XmlValue(true) val value: String = "")
|
||||
|
||||
@Serializable
|
||||
@XmlSerialName("Writer", "", "")
|
||||
data class Writer(@XmlValue(true) val value: String = "")
|
||||
|
||||
@Serializable
|
||||
@XmlSerialName("Penciller", "", "")
|
||||
data class Penciller(@XmlValue(true) val value: String = "")
|
||||
|
||||
@Serializable
|
||||
@XmlSerialName("Inker", "", "")
|
||||
data class Inker(@XmlValue(true) val value: String = "")
|
||||
|
||||
@Serializable
|
||||
@XmlSerialName("Colorist", "", "")
|
||||
data class Colorist(@XmlValue(true) val value: String = "")
|
||||
|
||||
@Serializable
|
||||
@XmlSerialName("Letterer", "", "")
|
||||
data class Letterer(@XmlValue(true) val value: String = "")
|
||||
|
||||
@Serializable
|
||||
@XmlSerialName("CoverArtist", "", "")
|
||||
data class CoverArtist(@XmlValue(true) val value: String = "")
|
||||
|
||||
@Serializable
|
||||
@XmlSerialName("Translator", "", "")
|
||||
data class Translator(@XmlValue(true) val value: String = "")
|
||||
|
||||
@Serializable
|
||||
@XmlSerialName("Genre", "", "")
|
||||
data class Genre(@XmlValue(true) val value: String = "")
|
||||
|
||||
@Serializable
|
||||
@XmlSerialName("Tags", "", "")
|
||||
data class Tags(@XmlValue(true) val value: String = "")
|
||||
|
||||
@Serializable
|
||||
@XmlSerialName("Web", "", "")
|
||||
data class Web(@XmlValue(true) val value: String = "")
|
||||
|
||||
// The spec doesn't have a good field for this
|
||||
@Serializable
|
||||
@XmlSerialName("PublishingStatusTachiyomi", "http://www.w3.org/2001/XMLSchema", "ty")
|
||||
data class PublishingStatusTachiyomi(@XmlValue(true) val value: String = "")
|
||||
}
|
||||
|
||||
private enum class ComicInfoPublishingStatus(
|
||||
val comicInfoValue: String,
|
||||
val sMangaModelValue: Int,
|
||||
) {
|
||||
ONGOING("Ongoing", SManga.ONGOING),
|
||||
COMPLETED("Completed", SManga.COMPLETED),
|
||||
LICENSED("Licensed", SManga.LICENSED),
|
||||
PUBLISHING_FINISHED("Publishing finished", SManga.PUBLISHING_FINISHED),
|
||||
CANCELLED("Cancelled", SManga.CANCELLED),
|
||||
ON_HIATUS("On hiatus", SManga.ON_HIATUS),
|
||||
UNKNOWN("Unknown", SManga.UNKNOWN),
|
||||
;
|
||||
|
||||
companion object {
|
||||
fun toComicInfoValue(value: Long): String {
|
||||
return values().firstOrNull { it.sMangaModelValue == value.toInt() }?.comicInfoValue
|
||||
?: UNKNOWN.comicInfoValue
|
||||
}
|
||||
|
||||
fun toSMangaValue(value: String?): Int {
|
||||
return values().firstOrNull { it.comicInfoValue == value }?.sMangaModelValue
|
||||
?: UNKNOWN.sMangaModelValue
|
||||
}
|
||||
}
|
||||
}
|
93
app/src/main/java/eu/kanade/domain/manga/model/Manga.kt
Normal file
93
app/src/main/java/eu/kanade/domain/manga/model/Manga.kt
Normal file
@ -0,0 +1,93 @@
|
||||
package eu.kanade.domain.manga.model
|
||||
|
||||
import eu.kanade.domain.base.BasePreferences
|
||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||
import eu.kanade.tachiyomi.source.LocalSource
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.domain.manga.model.TriStateFilter
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
// TODO: move these into the domain model
|
||||
val Manga.readingModeType: Long
|
||||
get() = viewerFlags and ReadingModeType.MASK.toLong()
|
||||
|
||||
val Manga.orientationType: Long
|
||||
get() = viewerFlags and OrientationType.MASK.toLong()
|
||||
|
||||
val Manga.downloadedFilter: TriStateFilter
|
||||
get() {
|
||||
if (forceDownloaded()) return TriStateFilter.ENABLED_IS
|
||||
return when (downloadedFilterRaw) {
|
||||
Manga.CHAPTER_SHOW_DOWNLOADED -> TriStateFilter.ENABLED_IS
|
||||
Manga.CHAPTER_SHOW_NOT_DOWNLOADED -> TriStateFilter.ENABLED_NOT
|
||||
else -> TriStateFilter.DISABLED
|
||||
}
|
||||
}
|
||||
fun Manga.chaptersFiltered(): Boolean {
|
||||
return unreadFilter != TriStateFilter.DISABLED ||
|
||||
downloadedFilter != TriStateFilter.DISABLED ||
|
||||
bookmarkedFilter != TriStateFilter.DISABLED
|
||||
}
|
||||
fun Manga.forceDownloaded(): Boolean {
|
||||
return favorite && Injekt.get<BasePreferences>().downloadedOnly().get()
|
||||
}
|
||||
|
||||
fun Manga.toSManga(): SManga = SManga.create().also {
|
||||
it.url = url
|
||||
it.title = title
|
||||
it.artist = artist
|
||||
it.author = author
|
||||
it.description = description
|
||||
it.genre = genre.orEmpty().joinToString()
|
||||
it.status = status.toInt()
|
||||
it.thumbnail_url = thumbnailUrl
|
||||
it.initialized = initialized
|
||||
}
|
||||
|
||||
fun Manga.copyFrom(other: SManga): Manga {
|
||||
val author = other.author ?: author
|
||||
val artist = other.artist ?: artist
|
||||
val description = other.description ?: description
|
||||
val genres = if (other.genre != null) {
|
||||
other.getGenres()
|
||||
} else {
|
||||
genre
|
||||
}
|
||||
val thumbnailUrl = other.thumbnail_url ?: thumbnailUrl
|
||||
return this.copy(
|
||||
author = author,
|
||||
artist = artist,
|
||||
description = description,
|
||||
genre = genres,
|
||||
thumbnailUrl = thumbnailUrl,
|
||||
status = other.status.toLong(),
|
||||
updateStrategy = other.update_strategy,
|
||||
initialized = other.initialized && initialized,
|
||||
)
|
||||
}
|
||||
|
||||
fun SManga.toDomainManga(sourceId: Long): Manga {
|
||||
return Manga.create().copy(
|
||||
url = url,
|
||||
title = title,
|
||||
artist = artist,
|
||||
author = author,
|
||||
description = description,
|
||||
genre = getGenres(),
|
||||
status = status.toLong(),
|
||||
thumbnailUrl = thumbnail_url,
|
||||
updateStrategy = update_strategy,
|
||||
initialized = initialized,
|
||||
source = sourceId,
|
||||
)
|
||||
}
|
||||
|
||||
fun Manga.isLocal(): Boolean = source == LocalSource.ID
|
||||
|
||||
fun Manga.hasCustomCover(coverCache: CoverCache = Injekt.get()): Boolean {
|
||||
return coverCache.getCustomCoverFile(id).exists()
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
package eu.kanade.domain.source.interactor
|
||||
|
||||
import eu.kanade.domain.source.repository.SourceRepository
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import eu.kanade.tachiyomi.source.LocalSource
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import tachiyomi.domain.source.model.Pin
|
||||
import tachiyomi.domain.source.model.Pins
|
||||
import tachiyomi.domain.source.model.Source
|
||||
|
||||
class GetEnabledSources(
|
||||
private val repository: SourceRepository,
|
||||
private val preferences: SourcePreferences,
|
||||
) {
|
||||
|
||||
fun subscribe(): Flow<List<Source>> {
|
||||
return combine(
|
||||
preferences.pinnedSources().changes(),
|
||||
preferences.enabledLanguages().changes(),
|
||||
preferences.disabledSources().changes(),
|
||||
preferences.lastUsedSource().changes(),
|
||||
repository.getSources(),
|
||||
) { pinnedSourceIds, enabledLanguages, disabledSources, lastUsedSource, sources ->
|
||||
sources
|
||||
.filter { it.lang in enabledLanguages || it.id == LocalSource.ID }
|
||||
.filterNot { it.id.toString() in disabledSources }
|
||||
.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name })
|
||||
.flatMap {
|
||||
val flag = if ("${it.id}" in pinnedSourceIds) Pins.pinned else Pins.unpinned
|
||||
val source = it.copy(pin = flag)
|
||||
val toFlatten = mutableListOf(source)
|
||||
if (source.id == lastUsedSource) {
|
||||
toFlatten.add(source.copy(isUsedLast = true, pin = source.pin - Pin.Actual))
|
||||
}
|
||||
toFlatten
|
||||
}
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
}
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
package eu.kanade.domain.source.interactor
|
||||
|
||||
import eu.kanade.domain.source.repository.SourceRepository
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import tachiyomi.domain.source.model.Source
|
||||
|
||||
class GetLanguagesWithSources(
|
||||
private val repository: SourceRepository,
|
||||
private val preferences: SourcePreferences,
|
||||
) {
|
||||
|
||||
fun subscribe(): Flow<Map<String, List<Source>>> {
|
||||
return combine(
|
||||
preferences.enabledLanguages().changes(),
|
||||
preferences.disabledSources().changes(),
|
||||
repository.getOnlineSources(),
|
||||
) { enabledLanguage, disabledSource, onlineSources ->
|
||||
val sortedSources = onlineSources.sortedWith(
|
||||
compareBy<Source> { it.id.toString() in disabledSource }
|
||||
.thenBy(String.CASE_INSENSITIVE_ORDER) { it.name },
|
||||
)
|
||||
|
||||
sortedSources.groupBy { it.lang }
|
||||
.toSortedMap(
|
||||
compareBy<String> { it !in enabledLanguage }.then(LocaleHelper.comparator),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
package eu.kanade.domain.source.interactor
|
||||
|
||||
import eu.kanade.domain.source.model.SourcePagingSourceType
|
||||
import eu.kanade.domain.source.repository.SourceRepository
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
|
||||
class GetRemoteManga(
|
||||
private val repository: SourceRepository,
|
||||
) {
|
||||
|
||||
fun subscribe(sourceId: Long, query: String, filterList: FilterList): SourcePagingSourceType {
|
||||
return when (query) {
|
||||
QUERY_POPULAR -> repository.getPopular(sourceId)
|
||||
QUERY_LATEST -> repository.getLatest(sourceId)
|
||||
else -> repository.search(sourceId, query, filterList)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val QUERY_POPULAR = "eu.kanade.domain.source.interactor.POPULAR"
|
||||
const val QUERY_LATEST = "eu.kanade.domain.source.interactor.LATEST"
|
||||
}
|
||||
}
|
@ -0,0 +1,59 @@
|
||||
package eu.kanade.domain.source.interactor
|
||||
|
||||
import eu.kanade.domain.source.repository.SourceRepository
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import tachiyomi.domain.source.model.Source
|
||||
import java.text.Collator
|
||||
import java.util.Collections
|
||||
import java.util.Locale
|
||||
|
||||
class GetSourcesWithFavoriteCount(
|
||||
private val repository: SourceRepository,
|
||||
private val preferences: SourcePreferences,
|
||||
) {
|
||||
|
||||
fun subscribe(): Flow<List<Pair<Source, Long>>> {
|
||||
return combine(
|
||||
preferences.migrationSortingDirection().changes(),
|
||||
preferences.migrationSortingMode().changes(),
|
||||
repository.getSourcesWithFavoriteCount(),
|
||||
) { direction, mode, list ->
|
||||
list.sortedWith(sortFn(direction, mode))
|
||||
}
|
||||
}
|
||||
|
||||
private fun sortFn(
|
||||
direction: SetMigrateSorting.Direction,
|
||||
sorting: SetMigrateSorting.Mode,
|
||||
): java.util.Comparator<Pair<Source, Long>> {
|
||||
val locale = Locale.getDefault()
|
||||
val collator = Collator.getInstance(locale).apply {
|
||||
strength = Collator.PRIMARY
|
||||
}
|
||||
val sortFn: (Pair<Source, Long>, Pair<Source, Long>) -> Int = { a, b ->
|
||||
when (sorting) {
|
||||
SetMigrateSorting.Mode.ALPHABETICAL -> {
|
||||
when {
|
||||
a.first.isStub && b.first.isStub.not() -> -1
|
||||
b.first.isStub && a.first.isStub.not() -> 1
|
||||
else -> collator.compare(a.first.name.lowercase(locale), b.first.name.lowercase(locale))
|
||||
}
|
||||
}
|
||||
SetMigrateSorting.Mode.TOTAL -> {
|
||||
when {
|
||||
a.first.isStub && b.first.isStub.not() -> -1
|
||||
b.first.isStub && a.first.isStub.not() -> 1
|
||||
else -> a.second.compareTo(b.second)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return when (direction) {
|
||||
SetMigrateSorting.Direction.ASCENDING -> Comparator(sortFn)
|
||||
SetMigrateSorting.Direction.DESCENDING -> Collections.reverseOrder(sortFn)
|
||||
}
|
||||
}
|
||||
}
|
14
app/src/main/java/eu/kanade/domain/source/interactor/GetSourcesWithNonLibraryManga.kt
Normal file
14
app/src/main/java/eu/kanade/domain/source/interactor/GetSourcesWithNonLibraryManga.kt
Normal file
@ -0,0 +1,14 @@
|
||||
package eu.kanade.domain.source.interactor
|
||||
|
||||
import eu.kanade.domain.source.repository.SourceRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import tachiyomi.domain.source.model.SourceWithCount
|
||||
|
||||
class GetSourcesWithNonLibraryManga(
|
||||
private val repository: SourceRepository,
|
||||
) {
|
||||
|
||||
fun subscribe(): Flow<List<SourceWithCount>> {
|
||||
return repository.getSourcesWithNonLibraryManga()
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
package eu.kanade.domain.source.interactor
|
||||
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
|
||||
class SetMigrateSorting(
|
||||
private val preferences: SourcePreferences,
|
||||
) {
|
||||
|
||||
fun await(mode: Mode, direction: Direction) {
|
||||
preferences.migrationSortingMode().set(mode)
|
||||
preferences.migrationSortingDirection().set(direction)
|
||||
}
|
||||
|
||||
enum class Mode {
|
||||
ALPHABETICAL,
|
||||
TOTAL,
|
||||
}
|
||||
|
||||
enum class Direction {
|
||||
ASCENDING,
|
||||
DESCENDING,
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
package eu.kanade.domain.source.interactor
|
||||
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import tachiyomi.core.preference.getAndSet
|
||||
|
||||
class ToggleLanguage(
|
||||
val preferences: SourcePreferences,
|
||||
) {
|
||||
|
||||
fun await(language: String) {
|
||||
val isEnabled = language in preferences.enabledLanguages().get()
|
||||
preferences.enabledLanguages().getAndSet { enabled ->
|
||||
if (isEnabled) enabled.minus(language) else enabled.plus(language)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
package eu.kanade.domain.source.interactor
|
||||
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import tachiyomi.core.preference.getAndSet
|
||||
import tachiyomi.domain.source.model.Source
|
||||
|
||||
class ToggleSource(
|
||||
private val preferences: SourcePreferences,
|
||||
) {
|
||||
|
||||
fun await(source: Source, enable: Boolean = isEnabled(source.id)) {
|
||||
await(source.id, enable)
|
||||
}
|
||||
|
||||
fun await(sourceId: Long, enable: Boolean = isEnabled(sourceId)) {
|
||||
preferences.disabledSources().getAndSet { disabled ->
|
||||
if (enable) disabled.minus("$sourceId") else disabled.plus("$sourceId")
|
||||
}
|
||||
}
|
||||
|
||||
fun await(sourceIds: List<Long>, enable: Boolean) {
|
||||
val transformedSourceIds = sourceIds.map { it.toString() }
|
||||
preferences.disabledSources().getAndSet { disabled ->
|
||||
if (enable) disabled.minus(transformedSourceIds) else disabled.plus(transformedSourceIds)
|
||||
}
|
||||
}
|
||||
|
||||
private fun isEnabled(sourceId: Long): Boolean {
|
||||
return sourceId.toString() in preferences.disabledSources().get()
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
package eu.kanade.domain.source.interactor
|
||||
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import tachiyomi.core.preference.getAndSet
|
||||
import tachiyomi.domain.source.model.Source
|
||||
|
||||
class ToggleSourcePin(
|
||||
private val preferences: SourcePreferences,
|
||||
) {
|
||||
|
||||
fun await(source: Source) {
|
||||
val isPinned = source.id.toString() in preferences.pinnedSources().get()
|
||||
preferences.pinnedSources().getAndSet { pinned ->
|
||||
if (isPinned) pinned.minus("${source.id}") else pinned.plus("${source.id}")
|
||||
}
|
||||
}
|
||||
}
|
16
app/src/main/java/eu/kanade/domain/source/model/Source.kt
Normal file
16
app/src/main/java/eu/kanade/domain/source/model/Source.kt
Normal file
@ -0,0 +1,16 @@
|
||||
package eu.kanade.domain.source.model
|
||||
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||
import tachiyomi.domain.source.model.Source
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
val Source.icon: ImageBitmap?
|
||||
get() {
|
||||
return Injekt.get<ExtensionManager>().getAppIconForSource(id)
|
||||
?.toBitmap()
|
||||
?.asImageBitmap()
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
package eu.kanade.domain.source.model
|
||||
|
||||
import androidx.paging.PagingSource
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
|
||||
typealias SourcePagingSourceType = PagingSource<Long, SManga>
|
@ -0,0 +1,24 @@
|
||||
package eu.kanade.domain.source.repository
|
||||
|
||||
import eu.kanade.domain.source.model.SourcePagingSourceType
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import tachiyomi.domain.source.model.Source
|
||||
import tachiyomi.domain.source.model.SourceWithCount
|
||||
|
||||
interface SourceRepository {
|
||||
|
||||
fun getSources(): Flow<List<Source>>
|
||||
|
||||
fun getOnlineSources(): Flow<List<Source>>
|
||||
|
||||
fun getSourcesWithFavoriteCount(): Flow<List<Pair<Source, Long>>>
|
||||
|
||||
fun getSourcesWithNonLibraryManga(): Flow<List<SourceWithCount>>
|
||||
|
||||
fun search(sourceId: Long, query: String, filterList: FilterList): SourcePagingSourceType
|
||||
|
||||
fun getPopular(sourceId: Long): SourcePagingSourceType
|
||||
|
||||
fun getLatest(sourceId: Long): SourcePagingSourceType
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
package eu.kanade.domain.source.service
|
||||
|
||||
import eu.kanade.domain.source.interactor.SetMigrateSorting
|
||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
import tachiyomi.core.preference.PreferenceStore
|
||||
import tachiyomi.core.preference.getEnum
|
||||
import tachiyomi.domain.library.model.LibraryDisplayMode
|
||||
|
||||
class SourcePreferences(
|
||||
private val preferenceStore: PreferenceStore,
|
||||
) {
|
||||
|
||||
fun sourceDisplayMode() = preferenceStore.getObject("pref_display_mode_catalogue", LibraryDisplayMode.default, LibraryDisplayMode.Serializer::serialize, LibraryDisplayMode.Serializer::deserialize)
|
||||
|
||||
fun enabledLanguages() = preferenceStore.getStringSet("source_languages", LocaleHelper.getDefaultEnabledLanguages())
|
||||
|
||||
fun disabledSources() = preferenceStore.getStringSet("hidden_catalogues", emptySet())
|
||||
|
||||
fun pinnedSources() = preferenceStore.getStringSet("pinned_catalogues", emptySet())
|
||||
|
||||
fun lastUsedSource() = preferenceStore.getLong("last_catalogue_source", -1)
|
||||
|
||||
fun showNsfwSource() = preferenceStore.getBoolean("show_nsfw_source", true)
|
||||
|
||||
fun migrationSortingMode() = preferenceStore.getEnum("pref_migration_sorting", SetMigrateSorting.Mode.ALPHABETICAL)
|
||||
|
||||
fun migrationSortingDirection() = preferenceStore.getEnum("pref_migration_direction", SetMigrateSorting.Direction.ASCENDING)
|
||||
|
||||
fun extensionUpdatesCount() = preferenceStore.getInt("ext_updates_count", 0)
|
||||
|
||||
fun trustedSignatures() = preferenceStore.getStringSet("trusted_signatures", emptySet())
|
||||
|
||||
fun searchPinnedSourcesOnly() = preferenceStore.getBoolean("search_pinned_sources_only", false)
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
package eu.kanade.domain.track.interactor
|
||||
|
||||
import logcat.LogPriority
|
||||
import tachiyomi.core.util.system.logcat
|
||||
import tachiyomi.domain.track.repository.TrackRepository
|
||||
|
||||
class DeleteTrack(
|
||||
private val trackRepository: TrackRepository,
|
||||
) {
|
||||
|
||||
suspend fun await(mangaId: Long, syncId: Long) {
|
||||
try {
|
||||
trackRepository.delete(mangaId, syncId)
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR, e)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
package eu.kanade.domain.track.interactor
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import logcat.LogPriority
|
||||
import tachiyomi.core.util.system.logcat
|
||||
import tachiyomi.domain.track.model.Track
|
||||
import tachiyomi.domain.track.repository.TrackRepository
|
||||
|
||||
class GetTracks(
|
||||
private val trackRepository: TrackRepository,
|
||||
) {
|
||||
|
||||
suspend fun awaitOne(id: Long): Track? {
|
||||
return try {
|
||||
trackRepository.getTrackById(id)
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR, e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun await(mangaId: Long): List<Track> {
|
||||
return try {
|
||||
trackRepository.getTracksByMangaId(mangaId)
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR, e)
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
fun subscribe(mangaId: Long): Flow<List<Track>> {
|
||||
return trackRepository.getTracksByMangaIdAsFlow(mangaId)
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
package eu.kanade.domain.track.interactor
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import tachiyomi.domain.track.repository.TrackRepository
|
||||
|
||||
class GetTracksPerManga(
|
||||
private val trackRepository: TrackRepository,
|
||||
) {
|
||||
|
||||
fun subscribe(): Flow<Map<Long, List<Long>>> {
|
||||
return trackRepository.getTracksAsFlow().map { tracks ->
|
||||
tracks
|
||||
.groupBy { it.mangaId }
|
||||
.mapValues { entry ->
|
||||
entry.value.map { it.syncId }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
package eu.kanade.domain.track.interactor
|
||||
|
||||
import logcat.LogPriority
|
||||
import tachiyomi.core.util.system.logcat
|
||||
import tachiyomi.domain.track.model.Track
|
||||
import tachiyomi.domain.track.repository.TrackRepository
|
||||
|
||||
class InsertTrack(
|
||||
private val trackRepository: TrackRepository,
|
||||
) {
|
||||
|
||||
suspend fun await(track: Track) {
|
||||
try {
|
||||
trackRepository.insert(track)
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR, e)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun awaitAll(tracks: List<Track>) {
|
||||
try {
|
||||
trackRepository.insertAll(tracks)
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR, e)
|
||||
}
|
||||
}
|
||||
}
|
48
app/src/main/java/eu/kanade/domain/track/model/Track.kt
Normal file
48
app/src/main/java/eu/kanade/domain/track/model/Track.kt
Normal file
@ -0,0 +1,48 @@
|
||||
package eu.kanade.domain.track.model
|
||||
|
||||
import tachiyomi.domain.track.model.Track
|
||||
import eu.kanade.tachiyomi.data.database.models.Track as DbTrack
|
||||
|
||||
fun Track.copyPersonalFrom(other: Track): Track {
|
||||
return this.copy(
|
||||
lastChapterRead = other.lastChapterRead,
|
||||
score = other.score,
|
||||
status = other.status,
|
||||
startDate = other.startDate,
|
||||
finishDate = other.finishDate,
|
||||
)
|
||||
}
|
||||
|
||||
fun Track.toDbTrack(): DbTrack = DbTrack.create(syncId).also {
|
||||
it.id = id
|
||||
it.manga_id = mangaId
|
||||
it.media_id = remoteId
|
||||
it.library_id = libraryId
|
||||
it.title = title
|
||||
it.last_chapter_read = lastChapterRead.toFloat()
|
||||
it.total_chapters = totalChapters.toInt()
|
||||
it.status = status.toInt()
|
||||
it.score = score
|
||||
it.tracking_url = remoteUrl
|
||||
it.started_reading_date = startDate
|
||||
it.finished_reading_date = finishDate
|
||||
}
|
||||
|
||||
fun DbTrack.toDomainTrack(idRequired: Boolean = true): Track? {
|
||||
val trackId = id ?: if (idRequired.not()) -1 else return null
|
||||
return Track(
|
||||
id = trackId,
|
||||
mangaId = manga_id,
|
||||
syncId = sync_id.toLong(),
|
||||
remoteId = media_id,
|
||||
libraryId = library_id,
|
||||
title = title,
|
||||
lastChapterRead = last_chapter_read.toDouble(),
|
||||
totalChapters = total_chapters.toLong(),
|
||||
status = status.toLong(),
|
||||
score = score,
|
||||
remoteUrl = tracking_url,
|
||||
startDate = started_reading_date,
|
||||
finishDate = finished_reading_date,
|
||||
)
|
||||
}
|
@ -0,0 +1,81 @@
|
||||
package eu.kanade.domain.track.service
|
||||
|
||||
import android.content.Context
|
||||
import androidx.work.BackoffPolicy
|
||||
import androidx.work.Constraints
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.ExistingWorkPolicy
|
||||
import androidx.work.NetworkType
|
||||
import androidx.work.OneTimeWorkRequestBuilder
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.WorkerParameters
|
||||
import eu.kanade.domain.track.interactor.GetTracks
|
||||
import eu.kanade.domain.track.interactor.InsertTrack
|
||||
import eu.kanade.domain.track.model.toDbTrack
|
||||
import eu.kanade.domain.track.store.DelayedTrackingStore
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import logcat.LogPriority
|
||||
import tachiyomi.core.util.lang.withIOContext
|
||||
import tachiyomi.core.util.system.logcat
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class DelayedTrackingUpdateJob(context: Context, workerParams: WorkerParameters) :
|
||||
CoroutineWorker(context, workerParams) {
|
||||
|
||||
override suspend fun doWork(): Result {
|
||||
val getTracks = Injekt.get<GetTracks>()
|
||||
val insertTrack = Injekt.get<InsertTrack>()
|
||||
|
||||
val trackManager = Injekt.get<TrackManager>()
|
||||
val delayedTrackingStore = Injekt.get<DelayedTrackingStore>()
|
||||
|
||||
val results = withIOContext {
|
||||
delayedTrackingStore.getItems()
|
||||
.mapNotNull {
|
||||
val track = getTracks.awaitOne(it.trackId)
|
||||
if (track == null) {
|
||||
delayedTrackingStore.remove(it.trackId)
|
||||
}
|
||||
track?.copy(lastChapterRead = it.lastChapterRead.toDouble())
|
||||
}
|
||||
.mapNotNull { track ->
|
||||
try {
|
||||
val service = trackManager.getService(track.syncId)
|
||||
if (service != null && service.isLogged) {
|
||||
logcat(LogPriority.DEBUG) { "Updating delayed track item: ${track.id}, last chapter read: ${track.lastChapterRead}" }
|
||||
service.update(track.toDbTrack(), true)
|
||||
insertTrack.await(track)
|
||||
}
|
||||
delayedTrackingStore.remove(track.id)
|
||||
null
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR, e)
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return if (results.isNotEmpty()) Result.failure() else Result.success()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "DelayedTrackingUpdate"
|
||||
|
||||
fun setupTask(context: Context) {
|
||||
val constraints = Constraints.Builder()
|
||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||
.build()
|
||||
|
||||
val request = OneTimeWorkRequestBuilder<DelayedTrackingUpdateJob>()
|
||||
.setConstraints(constraints)
|
||||
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 20, TimeUnit.SECONDS)
|
||||
.addTag(TAG)
|
||||
.build()
|
||||
|
||||
WorkManager.getInstance(context)
|
||||
.enqueueUniqueWork(TAG, ExistingWorkPolicy.REPLACE, request)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
package eu.kanade.domain.track.service
|
||||
|
||||
import eu.kanade.tachiyomi.data.track.TrackService
|
||||
import eu.kanade.tachiyomi.data.track.anilist.Anilist
|
||||
import tachiyomi.core.preference.PreferenceStore
|
||||
|
||||
class TrackPreferences(
|
||||
private val preferenceStore: PreferenceStore,
|
||||
) {
|
||||
|
||||
fun trackUsername(sync: TrackService) = preferenceStore.getString(trackUsername(sync.id), "")
|
||||
|
||||
fun trackPassword(sync: TrackService) = preferenceStore.getString(trackPassword(sync.id), "")
|
||||
|
||||
fun setTrackCredentials(sync: TrackService, username: String, password: String) {
|
||||
trackUsername(sync).set(username)
|
||||
trackPassword(sync).set(password)
|
||||
}
|
||||
|
||||
fun trackToken(sync: TrackService) = preferenceStore.getString(trackToken(sync.id), "")
|
||||
|
||||
fun anilistScoreType() = preferenceStore.getString("anilist_score_type", Anilist.POINT_10)
|
||||
|
||||
fun autoUpdateTrack() = preferenceStore.getBoolean("pref_auto_update_manga_sync_key", true)
|
||||
|
||||
companion object {
|
||||
fun trackUsername(syncId: Long) = "pref_mangasync_username_$syncId"
|
||||
|
||||
private fun trackPassword(syncId: Long) = "pref_mangasync_password_$syncId"
|
||||
|
||||
private fun trackToken(syncId: Long) = "track_token_$syncId"
|
||||
}
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
package eu.kanade.domain.track.store
|
||||
|
||||
import android.content.Context
|
||||
import androidx.core.content.edit
|
||||
import logcat.LogPriority
|
||||
import tachiyomi.core.util.system.logcat
|
||||
import tachiyomi.domain.track.model.Track
|
||||
|
||||
class DelayedTrackingStore(context: Context) {
|
||||
|
||||
/**
|
||||
* Preference file where queued tracking updates are stored.
|
||||
*/
|
||||
private val preferences = context.getSharedPreferences("tracking_queue", Context.MODE_PRIVATE)
|
||||
|
||||
fun addItem(track: Track) {
|
||||
val trackId = track.id.toString()
|
||||
val lastChapterRead = preferences.getFloat(trackId, 0f)
|
||||
if (track.lastChapterRead > lastChapterRead) {
|
||||
logcat(LogPriority.DEBUG) { "Queuing track item: $trackId, last chapter read: ${track.lastChapterRead}" }
|
||||
preferences.edit {
|
||||
putFloat(trackId, track.lastChapterRead.toFloat())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun remove(trackId: Long) {
|
||||
preferences.edit {
|
||||
remove(trackId.toString())
|
||||
}
|
||||
}
|
||||
|
||||
fun getItems(): List<DelayedTrackingItem> {
|
||||
return preferences.all.mapNotNull {
|
||||
DelayedTrackingItem(
|
||||
trackId = it.key.toLong(),
|
||||
lastChapterRead = it.value.toString().toFloat(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class DelayedTrackingItem(
|
||||
val trackId: Long,
|
||||
val lastChapterRead: Float,
|
||||
)
|
||||
}
|
43
app/src/main/java/eu/kanade/domain/ui/UiPreferences.kt
Normal file
43
app/src/main/java/eu/kanade/domain/ui/UiPreferences.kt
Normal file
@ -0,0 +1,43 @@
|
||||
package eu.kanade.domain.ui
|
||||
|
||||
import android.os.Build
|
||||
import eu.kanade.domain.ui.model.AppTheme
|
||||
import eu.kanade.domain.ui.model.TabletUiMode
|
||||
import eu.kanade.domain.ui.model.ThemeMode
|
||||
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
||||
import eu.kanade.tachiyomi.util.system.isDynamicColorAvailable
|
||||
import tachiyomi.core.preference.PreferenceStore
|
||||
import tachiyomi.core.preference.getEnum
|
||||
import java.text.DateFormat
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
class UiPreferences(
|
||||
private val preferenceStore: PreferenceStore,
|
||||
) {
|
||||
|
||||
fun themeMode() = preferenceStore.getEnum(
|
||||
"pref_theme_mode_key",
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { ThemeMode.SYSTEM } else { ThemeMode.LIGHT },
|
||||
)
|
||||
|
||||
fun appTheme() = preferenceStore.getEnum(
|
||||
"pref_app_theme",
|
||||
if (DeviceUtil.isDynamicColorAvailable) { AppTheme.MONET } else { AppTheme.DEFAULT },
|
||||
)
|
||||
|
||||
fun themeDarkAmoled() = preferenceStore.getBoolean("pref_theme_dark_amoled_key", false)
|
||||
|
||||
fun relativeTime() = preferenceStore.getInt("relative_time", 7)
|
||||
|
||||
fun dateFormat() = preferenceStore.getString("app_date_format", "")
|
||||
|
||||
fun tabletUiMode() = preferenceStore.getEnum("tablet_ui_mode", TabletUiMode.AUTOMATIC)
|
||||
|
||||
companion object {
|
||||
fun dateFormat(format: String): DateFormat = when (format) {
|
||||
"" -> DateFormat.getDateInstance(DateFormat.SHORT)
|
||||
else -> SimpleDateFormat(format, Locale.getDefault())
|
||||
}
|
||||
}
|
||||
}
|
22
app/src/main/java/eu/kanade/domain/ui/model/AppTheme.kt
Normal file
22
app/src/main/java/eu/kanade/domain/ui/model/AppTheme.kt
Normal file
@ -0,0 +1,22 @@
|
||||
package eu.kanade.domain.ui.model
|
||||
|
||||
import eu.kanade.tachiyomi.R
|
||||
|
||||
enum class AppTheme(val titleResId: Int?) {
|
||||
DEFAULT(R.string.label_default),
|
||||
MONET(R.string.theme_monet),
|
||||
GREEN_APPLE(R.string.theme_greenapple),
|
||||
LAVENDER(R.string.theme_lavender),
|
||||
MIDNIGHT_DUSK(R.string.theme_midnightdusk),
|
||||
STRAWBERRY_DAIQUIRI(R.string.theme_strawberrydaiquiri),
|
||||
TAKO(R.string.theme_tako),
|
||||
TEALTURQUOISE(R.string.theme_tealturquoise),
|
||||
TIDAL_WAVE(R.string.theme_tidalwave),
|
||||
YINYANG(R.string.theme_yinyang),
|
||||
YOTSUBA(R.string.theme_yotsuba),
|
||||
|
||||
// Deprecated
|
||||
DARK_BLUE(null),
|
||||
HOT_PINK(null),
|
||||
BLUE(null),
|
||||
}
|
10
app/src/main/java/eu/kanade/domain/ui/model/TabletUiMode.kt
Normal file
10
app/src/main/java/eu/kanade/domain/ui/model/TabletUiMode.kt
Normal file
@ -0,0 +1,10 @@
|
||||
package eu.kanade.domain.ui.model
|
||||
|
||||
import eu.kanade.tachiyomi.R
|
||||
|
||||
enum class TabletUiMode(val titleResId: Int) {
|
||||
AUTOMATIC(R.string.automatic_background),
|
||||
ALWAYS(R.string.lock_always),
|
||||
LANDSCAPE(R.string.landscape),
|
||||
NEVER(R.string.lock_never),
|
||||
}
|
19
app/src/main/java/eu/kanade/domain/ui/model/ThemeMode.kt
Normal file
19
app/src/main/java/eu/kanade/domain/ui/model/ThemeMode.kt
Normal file
@ -0,0 +1,19 @@
|
||||
package eu.kanade.domain.ui.model
|
||||
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
|
||||
enum class ThemeMode {
|
||||
LIGHT,
|
||||
DARK,
|
||||
SYSTEM,
|
||||
}
|
||||
|
||||
fun setAppCompatDelegateThemeMode(themeMode: ThemeMode) {
|
||||
AppCompatDelegate.setDefaultNightMode(
|
||||
when (themeMode) {
|
||||
ThemeMode.LIGHT -> AppCompatDelegate.MODE_NIGHT_NO
|
||||
ThemeMode.DARK -> AppCompatDelegate.MODE_NIGHT_YES
|
||||
ThemeMode.SYSTEM -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
|
||||
},
|
||||
)
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
package eu.kanade.presentation.browse
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import eu.kanade.presentation.components.Badge
|
||||
import eu.kanade.tachiyomi.R
|
||||
|
||||
@Composable
|
||||
fun InLibraryBadge(enabled: Boolean) {
|
||||
if (enabled) {
|
||||
Badge(text = stringResource(R.string.in_library))
|
||||
}
|
||||
}
|
@ -0,0 +1,167 @@
|
||||
package eu.kanade.presentation.browse
|
||||
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.HelpOutline
|
||||
import androidx.compose.material.icons.outlined.Public
|
||||
import androidx.compose.material.icons.outlined.Refresh
|
||||
import androidx.compose.material3.SnackbarDuration
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.SnackbarResult
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.paging.LoadState
|
||||
import androidx.paging.compose.LazyPagingItems
|
||||
import eu.kanade.data.source.NoResultsException
|
||||
import eu.kanade.presentation.browse.components.BrowseSourceComfortableGrid
|
||||
import eu.kanade.presentation.browse.components.BrowseSourceCompactGrid
|
||||
import eu.kanade.presentation.browse.components.BrowseSourceList
|
||||
import eu.kanade.presentation.components.AppBar
|
||||
import eu.kanade.presentation.components.EmptyScreen
|
||||
import eu.kanade.presentation.components.EmptyScreenAction
|
||||
import eu.kanade.presentation.components.LoadingScreen
|
||||
import eu.kanade.presentation.components.Scaffold
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.source.LocalSource
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import tachiyomi.domain.library.model.LibraryDisplayMode
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
|
||||
@Composable
|
||||
fun BrowseSourceContent(
|
||||
source: Source?,
|
||||
mangaList: LazyPagingItems<StateFlow<Manga>>,
|
||||
columns: GridCells,
|
||||
displayMode: LibraryDisplayMode,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
contentPadding: PaddingValues,
|
||||
onWebViewClick: () -> Unit,
|
||||
onHelpClick: () -> Unit,
|
||||
onLocalSourceHelpClick: () -> Unit,
|
||||
onMangaClick: (Manga) -> Unit,
|
||||
onMangaLongClick: (Manga) -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
val errorState = mangaList.loadState.refresh.takeIf { it is LoadState.Error }
|
||||
?: mangaList.loadState.append.takeIf { it is LoadState.Error }
|
||||
|
||||
val getErrorMessage: (LoadState.Error) -> String = { state ->
|
||||
when {
|
||||
state.error is NoResultsException -> context.getString(R.string.no_results_found)
|
||||
state.error.message.isNullOrEmpty() -> ""
|
||||
state.error.message.orEmpty().startsWith("HTTP error") -> "${state.error.message}: ${context.getString(R.string.http_error_hint)}"
|
||||
else -> state.error.message.orEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(errorState) {
|
||||
if (mangaList.itemCount > 0 && errorState != null && errorState is LoadState.Error) {
|
||||
val result = snackbarHostState.showSnackbar(
|
||||
message = getErrorMessage(errorState),
|
||||
actionLabel = context.getString(R.string.action_webview_refresh),
|
||||
duration = SnackbarDuration.Indefinite,
|
||||
)
|
||||
when (result) {
|
||||
SnackbarResult.Dismissed -> snackbarHostState.currentSnackbarData?.dismiss()
|
||||
SnackbarResult.ActionPerformed -> mangaList.refresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (mangaList.itemCount <= 0 && errorState != null && errorState is LoadState.Error) {
|
||||
EmptyScreen(
|
||||
message = getErrorMessage(errorState),
|
||||
actions = if (source is LocalSource) {
|
||||
listOf(
|
||||
EmptyScreenAction(
|
||||
stringResId = R.string.local_source_help_guide,
|
||||
icon = Icons.Outlined.HelpOutline,
|
||||
onClick = onLocalSourceHelpClick,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
listOf(
|
||||
EmptyScreenAction(
|
||||
stringResId = R.string.action_retry,
|
||||
icon = Icons.Outlined.Refresh,
|
||||
onClick = mangaList::refresh,
|
||||
),
|
||||
EmptyScreenAction(
|
||||
stringResId = R.string.action_open_in_web_view,
|
||||
icon = Icons.Outlined.Public,
|
||||
onClick = onWebViewClick,
|
||||
),
|
||||
EmptyScreenAction(
|
||||
stringResId = R.string.label_help,
|
||||
icon = Icons.Outlined.HelpOutline,
|
||||
onClick = onHelpClick,
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (mangaList.itemCount == 0 && mangaList.loadState.refresh is LoadState.Loading) {
|
||||
LoadingScreen()
|
||||
return
|
||||
}
|
||||
|
||||
when (displayMode) {
|
||||
LibraryDisplayMode.ComfortableGrid -> {
|
||||
BrowseSourceComfortableGrid(
|
||||
mangaList = mangaList,
|
||||
columns = columns,
|
||||
contentPadding = contentPadding,
|
||||
onMangaClick = onMangaClick,
|
||||
onMangaLongClick = onMangaLongClick,
|
||||
)
|
||||
}
|
||||
LibraryDisplayMode.List -> {
|
||||
BrowseSourceList(
|
||||
mangaList = mangaList,
|
||||
contentPadding = contentPadding,
|
||||
onMangaClick = onMangaClick,
|
||||
onMangaLongClick = onMangaLongClick,
|
||||
)
|
||||
}
|
||||
LibraryDisplayMode.CompactGrid, LibraryDisplayMode.CoverOnlyGrid -> {
|
||||
BrowseSourceCompactGrid(
|
||||
mangaList = mangaList,
|
||||
columns = columns,
|
||||
contentPadding = contentPadding,
|
||||
onMangaClick = onMangaClick,
|
||||
onMangaLongClick = onMangaLongClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MissingSourceScreen(
|
||||
source: SourceManager.StubSource,
|
||||
navigateUp: () -> Unit,
|
||||
) {
|
||||
Scaffold(
|
||||
topBar = { scrollBehavior ->
|
||||
AppBar(
|
||||
title = source.name,
|
||||
navigateUp = navigateUp,
|
||||
scrollBehavior = scrollBehavior,
|
||||
)
|
||||
},
|
||||
) { paddingValues ->
|
||||
EmptyScreen(
|
||||
message = source.getSourceNotInstalledException().message!!,
|
||||
modifier = Modifier.padding(paddingValues),
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,423 @@
|
||||
package eu.kanade.presentation.browse
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.provider.Settings
|
||||
import android.util.DisplayMetrics
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.HelpOutline
|
||||
import androidx.compose.material.icons.outlined.History
|
||||
import androidx.compose.material.icons.outlined.Settings
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.kanade.domain.extension.interactor.ExtensionSourceItem
|
||||
import eu.kanade.presentation.browse.components.ExtensionIcon
|
||||
import eu.kanade.presentation.components.AppBar
|
||||
import eu.kanade.presentation.components.AppBarActions
|
||||
import eu.kanade.presentation.components.DIVIDER_ALPHA
|
||||
import eu.kanade.presentation.components.Divider
|
||||
import eu.kanade.presentation.components.EmptyScreen
|
||||
import eu.kanade.presentation.components.Scaffold
|
||||
import eu.kanade.presentation.components.ScrollbarLazyColumn
|
||||
import eu.kanade.presentation.components.WarningBanner
|
||||
import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget
|
||||
import eu.kanade.presentation.more.settings.widget.TrailingWidgetBuffer
|
||||
import eu.kanade.presentation.util.padding
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.extension.model.Extension
|
||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||
import eu.kanade.tachiyomi.ui.browse.extension.details.ExtensionDetailsState
|
||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
|
||||
@Composable
|
||||
fun ExtensionDetailsScreen(
|
||||
navigateUp: () -> Unit,
|
||||
state: ExtensionDetailsState,
|
||||
onClickSourcePreferences: (sourceId: Long) -> Unit,
|
||||
onClickWhatsNew: () -> Unit,
|
||||
onClickReadme: () -> Unit,
|
||||
onClickEnableAll: () -> Unit,
|
||||
onClickDisableAll: () -> Unit,
|
||||
onClickClearCookies: () -> Unit,
|
||||
onClickUninstall: () -> Unit,
|
||||
onClickSource: (sourceId: Long) -> Unit,
|
||||
) {
|
||||
Scaffold(
|
||||
topBar = { scrollBehavior ->
|
||||
AppBar(
|
||||
title = stringResource(R.string.label_extension_info),
|
||||
navigateUp = navigateUp,
|
||||
actions = {
|
||||
AppBarActions(
|
||||
actions = buildList {
|
||||
if (state.extension?.isUnofficial == false) {
|
||||
add(
|
||||
AppBar.Action(
|
||||
title = stringResource(R.string.whats_new),
|
||||
icon = Icons.Outlined.History,
|
||||
onClick = onClickWhatsNew,
|
||||
),
|
||||
)
|
||||
add(
|
||||
AppBar.Action(
|
||||
title = stringResource(R.string.action_faq_and_guides),
|
||||
icon = Icons.Outlined.HelpOutline,
|
||||
onClick = onClickReadme,
|
||||
),
|
||||
)
|
||||
}
|
||||
addAll(
|
||||
listOf(
|
||||
AppBar.OverflowAction(
|
||||
title = stringResource(R.string.action_enable_all),
|
||||
onClick = onClickEnableAll,
|
||||
),
|
||||
AppBar.OverflowAction(
|
||||
title = stringResource(R.string.action_disable_all),
|
||||
onClick = onClickDisableAll,
|
||||
),
|
||||
AppBar.OverflowAction(
|
||||
title = stringResource(R.string.pref_clear_cookies),
|
||||
onClick = onClickClearCookies,
|
||||
),
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
scrollBehavior = scrollBehavior,
|
||||
)
|
||||
},
|
||||
) { paddingValues ->
|
||||
if (state.extension == null) {
|
||||
EmptyScreen(
|
||||
textResource = R.string.empty_screen,
|
||||
modifier = Modifier.padding(paddingValues),
|
||||
)
|
||||
return@Scaffold
|
||||
}
|
||||
|
||||
ExtensionDetails(
|
||||
contentPadding = paddingValues,
|
||||
extension = state.extension,
|
||||
sources = state.sources,
|
||||
onClickSourcePreferences = onClickSourcePreferences,
|
||||
onClickUninstall = onClickUninstall,
|
||||
onClickSource = onClickSource,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ExtensionDetails(
|
||||
contentPadding: PaddingValues,
|
||||
extension: Extension.Installed,
|
||||
sources: List<ExtensionSourceItem>,
|
||||
onClickSourcePreferences: (sourceId: Long) -> Unit,
|
||||
onClickUninstall: () -> Unit,
|
||||
onClickSource: (sourceId: Long) -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
var showNsfwWarning by remember { mutableStateOf(false) }
|
||||
|
||||
ScrollbarLazyColumn(
|
||||
contentPadding = contentPadding,
|
||||
) {
|
||||
when {
|
||||
extension.isUnofficial ->
|
||||
item {
|
||||
WarningBanner(R.string.unofficial_extension_message)
|
||||
}
|
||||
extension.isObsolete ->
|
||||
item {
|
||||
WarningBanner(R.string.obsolete_extension_message)
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
DetailsHeader(
|
||||
extension = extension,
|
||||
onClickUninstall = onClickUninstall,
|
||||
onClickAppInfo = {
|
||||
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
|
||||
data = Uri.fromParts("package", extension.pkgName, null)
|
||||
context.startActivity(this)
|
||||
}
|
||||
},
|
||||
onClickAgeRating = {
|
||||
showNsfwWarning = true
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
items(
|
||||
items = sources,
|
||||
key = { it.source.id },
|
||||
) { source ->
|
||||
SourceSwitchPreference(
|
||||
modifier = Modifier.animateItemPlacement(),
|
||||
source = source,
|
||||
onClickSourcePreferences = onClickSourcePreferences,
|
||||
onClickSource = onClickSource,
|
||||
)
|
||||
}
|
||||
}
|
||||
if (showNsfwWarning) {
|
||||
NsfwWarningDialog(
|
||||
onClickConfirm = {
|
||||
showNsfwWarning = false
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DetailsHeader(
|
||||
extension: Extension,
|
||||
onClickAgeRating: () -> Unit,
|
||||
onClickUninstall: () -> Unit,
|
||||
onClickAppInfo: () -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
Column {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
start = MaterialTheme.padding.medium,
|
||||
end = MaterialTheme.padding.medium,
|
||||
top = MaterialTheme.padding.medium,
|
||||
bottom = MaterialTheme.padding.small,
|
||||
),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
ExtensionIcon(
|
||||
modifier = Modifier
|
||||
.size(112.dp),
|
||||
extension = extension,
|
||||
density = DisplayMetrics.DENSITY_XXXHIGH,
|
||||
)
|
||||
|
||||
Text(
|
||||
text = extension.name,
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
|
||||
val strippedPkgName = extension.pkgName.substringAfter("eu.kanade.tachiyomi.extension.")
|
||||
|
||||
Text(
|
||||
text = strippedPkgName,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
horizontal = MaterialTheme.padding.extraLarge,
|
||||
vertical = MaterialTheme.padding.small,
|
||||
),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
InfoText(
|
||||
modifier = Modifier.weight(1f),
|
||||
primaryText = extension.versionName,
|
||||
secondaryText = stringResource(R.string.ext_info_version),
|
||||
)
|
||||
|
||||
InfoDivider()
|
||||
|
||||
InfoText(
|
||||
modifier = Modifier.weight(if (extension.isNsfw) 1.5f else 1f),
|
||||
primaryText = LocaleHelper.getSourceDisplayName(extension.lang, context),
|
||||
secondaryText = stringResource(R.string.ext_info_language),
|
||||
)
|
||||
|
||||
if (extension.isNsfw) {
|
||||
InfoDivider()
|
||||
|
||||
InfoText(
|
||||
modifier = Modifier.weight(1f),
|
||||
primaryText = stringResource(R.string.ext_nsfw_short),
|
||||
primaryTextStyle = MaterialTheme.typography.bodyLarge.copy(
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
fontWeight = FontWeight.Medium,
|
||||
),
|
||||
secondaryText = stringResource(R.string.ext_info_age_rating),
|
||||
onClick = onClickAgeRating,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier.padding(
|
||||
start = MaterialTheme.padding.medium,
|
||||
end = MaterialTheme.padding.medium,
|
||||
top = MaterialTheme.padding.small,
|
||||
bottom = MaterialTheme.padding.medium,
|
||||
),
|
||||
) {
|
||||
OutlinedButton(
|
||||
modifier = Modifier.weight(1f),
|
||||
onClick = onClickUninstall,
|
||||
) {
|
||||
Text(stringResource(R.string.ext_uninstall))
|
||||
}
|
||||
|
||||
Spacer(Modifier.width(16.dp))
|
||||
|
||||
Button(
|
||||
modifier = Modifier.weight(1f),
|
||||
onClick = onClickAppInfo,
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.ext_app_info),
|
||||
color = MaterialTheme.colorScheme.onPrimary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Divider()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun InfoText(
|
||||
modifier: Modifier,
|
||||
primaryText: String,
|
||||
primaryTextStyle: TextStyle = MaterialTheme.typography.bodyLarge,
|
||||
secondaryText: String,
|
||||
onClick: (() -> Unit)? = null,
|
||||
) {
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
|
||||
val clickableModifier = if (onClick != null) {
|
||||
Modifier.clickable(interactionSource, indication = null) { onClick() }
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = modifier.then(clickableModifier),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
) {
|
||||
Text(
|
||||
text = primaryText,
|
||||
textAlign = TextAlign.Center,
|
||||
style = primaryTextStyle,
|
||||
)
|
||||
|
||||
Text(
|
||||
text = secondaryText + if (onClick != null) " ⓘ" else "",
|
||||
textAlign = TextAlign.Center,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun InfoDivider() {
|
||||
Divider(
|
||||
modifier = Modifier
|
||||
.height(20.dp)
|
||||
.width(1.dp),
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = DIVIDER_ALPHA),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SourceSwitchPreference(
|
||||
modifier: Modifier = Modifier,
|
||||
source: ExtensionSourceItem,
|
||||
onClickSourcePreferences: (sourceId: Long) -> Unit,
|
||||
onClickSource: (sourceId: Long) -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
TextPreferenceWidget(
|
||||
modifier = modifier,
|
||||
title = if (source.labelAsName) {
|
||||
source.source.toString()
|
||||
} else {
|
||||
LocaleHelper.getSourceDisplayName(source.source.lang, context)
|
||||
},
|
||||
widget = {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
if (source.source is ConfigurableSource) {
|
||||
IconButton(onClick = { onClickSourcePreferences(source.source.id) }) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Settings,
|
||||
contentDescription = stringResource(R.string.label_settings),
|
||||
tint = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Switch(
|
||||
checked = source.enabled,
|
||||
onCheckedChange = null,
|
||||
modifier = Modifier.padding(start = TrailingWidgetBuffer),
|
||||
)
|
||||
}
|
||||
},
|
||||
onPreferenceClick = { onClickSource(source.source.id) },
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NsfwWarningDialog(
|
||||
onClickConfirm: () -> Unit,
|
||||
) {
|
||||
AlertDialog(
|
||||
text = {
|
||||
Text(text = stringResource(R.string.ext_nsfw_warning))
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = onClickConfirm) {
|
||||
Text(text = stringResource(android.R.string.ok))
|
||||
}
|
||||
},
|
||||
onDismissRequest = onClickConfirm,
|
||||
)
|
||||
}
|
@ -0,0 +1,68 @@
|
||||
package eu.kanade.presentation.browse
|
||||
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import eu.kanade.presentation.components.AppBar
|
||||
import eu.kanade.presentation.components.EmptyScreen
|
||||
import eu.kanade.presentation.components.FastScrollLazyColumn
|
||||
import eu.kanade.presentation.components.Scaffold
|
||||
import eu.kanade.presentation.more.settings.widget.SwitchPreferenceWidget
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionFilterState
|
||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
|
||||
@Composable
|
||||
fun ExtensionFilterScreen(
|
||||
navigateUp: () -> Unit,
|
||||
state: ExtensionFilterState.Success,
|
||||
onClickToggle: (String) -> Unit,
|
||||
) {
|
||||
Scaffold(
|
||||
topBar = { scrollBehavior ->
|
||||
AppBar(
|
||||
title = stringResource(R.string.label_extensions),
|
||||
navigateUp = navigateUp,
|
||||
scrollBehavior = scrollBehavior,
|
||||
)
|
||||
},
|
||||
) { contentPadding ->
|
||||
if (state.isEmpty) {
|
||||
EmptyScreen(
|
||||
textResource = R.string.empty_screen,
|
||||
modifier = Modifier.padding(contentPadding),
|
||||
)
|
||||
return@Scaffold
|
||||
}
|
||||
ExtensionFilterContent(
|
||||
contentPadding = contentPadding,
|
||||
state = state,
|
||||
onClickLang = onClickToggle,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ExtensionFilterContent(
|
||||
contentPadding: PaddingValues,
|
||||
state: ExtensionFilterState.Success,
|
||||
onClickLang: (String) -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
FastScrollLazyColumn(
|
||||
contentPadding = contentPadding,
|
||||
) {
|
||||
items(state.languages) { language ->
|
||||
SwitchPreferenceWidget(
|
||||
modifier = Modifier.animateItemPlacement(),
|
||||
title = LocaleHelper.getSourceDisplayName(language, context),
|
||||
checked = language in state.enabledLanguages,
|
||||
onCheckedChanged = { onClickLang(language) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,442 @@
|
||||
package eu.kanade.presentation.browse
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Close
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ProvideTextStyle
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.google.accompanist.flowlayout.FlowRow
|
||||
import eu.kanade.presentation.browse.components.BaseBrowseItem
|
||||
import eu.kanade.presentation.browse.components.ExtensionIcon
|
||||
import eu.kanade.presentation.components.EmptyScreen
|
||||
import eu.kanade.presentation.components.FastScrollLazyColumn
|
||||
import eu.kanade.presentation.components.LoadingScreen
|
||||
import eu.kanade.presentation.components.PullRefresh
|
||||
import eu.kanade.presentation.manga.components.DotSeparatorNoSpaceText
|
||||
import eu.kanade.presentation.theme.header
|
||||
import eu.kanade.presentation.util.padding
|
||||
import eu.kanade.presentation.util.plus
|
||||
import eu.kanade.presentation.util.secondaryItemAlpha
|
||||
import eu.kanade.presentation.util.topSmallPaddingValues
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.extension.model.Extension
|
||||
import eu.kanade.tachiyomi.extension.model.InstallStep
|
||||
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionUiModel
|
||||
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionsState
|
||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
|
||||
@Composable
|
||||
fun ExtensionScreen(
|
||||
state: ExtensionsState,
|
||||
contentPadding: PaddingValues,
|
||||
searchQuery: String?,
|
||||
onLongClickItem: (Extension) -> Unit,
|
||||
onClickItemCancel: (Extension) -> Unit,
|
||||
onInstallExtension: (Extension.Available) -> Unit,
|
||||
onUninstallExtension: (Extension) -> Unit,
|
||||
onUpdateExtension: (Extension.Installed) -> Unit,
|
||||
onTrustExtension: (Extension.Untrusted) -> Unit,
|
||||
onOpenExtension: (Extension.Installed) -> Unit,
|
||||
onClickUpdateAll: () -> Unit,
|
||||
onRefresh: () -> Unit,
|
||||
) {
|
||||
PullRefresh(
|
||||
refreshing = state.isRefreshing,
|
||||
onRefresh = onRefresh,
|
||||
enabled = !state.isLoading,
|
||||
) {
|
||||
when {
|
||||
state.isLoading -> LoadingScreen(modifier = Modifier.padding(contentPadding))
|
||||
state.isEmpty -> {
|
||||
val msg = if (!searchQuery.isNullOrEmpty()) {
|
||||
R.string.no_results_found
|
||||
} else {
|
||||
R.string.empty_screen
|
||||
}
|
||||
EmptyScreen(
|
||||
textResource = msg,
|
||||
modifier = Modifier.padding(contentPadding),
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
ExtensionContent(
|
||||
state = state,
|
||||
contentPadding = contentPadding,
|
||||
onLongClickItem = onLongClickItem,
|
||||
onClickItemCancel = onClickItemCancel,
|
||||
onInstallExtension = onInstallExtension,
|
||||
onUninstallExtension = onUninstallExtension,
|
||||
onUpdateExtension = onUpdateExtension,
|
||||
onTrustExtension = onTrustExtension,
|
||||
onOpenExtension = onOpenExtension,
|
||||
onClickUpdateAll = onClickUpdateAll,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ExtensionContent(
|
||||
state: ExtensionsState,
|
||||
contentPadding: PaddingValues,
|
||||
onLongClickItem: (Extension) -> Unit,
|
||||
onClickItemCancel: (Extension) -> Unit,
|
||||
onInstallExtension: (Extension.Available) -> Unit,
|
||||
onUninstallExtension: (Extension) -> Unit,
|
||||
onUpdateExtension: (Extension.Installed) -> Unit,
|
||||
onTrustExtension: (Extension.Untrusted) -> Unit,
|
||||
onOpenExtension: (Extension.Installed) -> Unit,
|
||||
onClickUpdateAll: () -> Unit,
|
||||
) {
|
||||
var trustState by remember { mutableStateOf<Extension.Untrusted?>(null) }
|
||||
|
||||
FastScrollLazyColumn(
|
||||
contentPadding = contentPadding + topSmallPaddingValues,
|
||||
) {
|
||||
state.items.forEach { (header, items) ->
|
||||
item(
|
||||
contentType = "header",
|
||||
key = "extensionHeader-${header.hashCode()}",
|
||||
) {
|
||||
when (header) {
|
||||
is ExtensionUiModel.Header.Resource -> {
|
||||
val action: @Composable RowScope.() -> Unit =
|
||||
if (header.textRes == R.string.ext_updates_pending) {
|
||||
{
|
||||
Button(onClick = { onClickUpdateAll() }) {
|
||||
Text(
|
||||
text = stringResource(R.string.ext_update_all),
|
||||
style = LocalTextStyle.current.copy(
|
||||
color = MaterialTheme.colorScheme.onPrimary,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
{}
|
||||
}
|
||||
ExtensionHeader(
|
||||
textRes = header.textRes,
|
||||
modifier = Modifier.animateItemPlacement(),
|
||||
action = action,
|
||||
)
|
||||
}
|
||||
is ExtensionUiModel.Header.Text -> {
|
||||
ExtensionHeader(
|
||||
text = header.text,
|
||||
modifier = Modifier.animateItemPlacement(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
items(
|
||||
items = items,
|
||||
contentType = { "item" },
|
||||
key = { "extension-${it.hashCode()}" },
|
||||
) { item ->
|
||||
ExtensionItem(
|
||||
modifier = Modifier.animateItemPlacement(),
|
||||
item = item,
|
||||
onClickItem = {
|
||||
when (it) {
|
||||
is Extension.Available -> onInstallExtension(it)
|
||||
is Extension.Installed -> onOpenExtension(it)
|
||||
is Extension.Untrusted -> { trustState = it }
|
||||
}
|
||||
},
|
||||
onLongClickItem = onLongClickItem,
|
||||
onClickItemCancel = onClickItemCancel,
|
||||
onClickItemAction = {
|
||||
when (it) {
|
||||
is Extension.Available -> onInstallExtension(it)
|
||||
is Extension.Installed -> {
|
||||
if (it.hasUpdate) {
|
||||
onUpdateExtension(it)
|
||||
} else {
|
||||
onOpenExtension(it)
|
||||
}
|
||||
}
|
||||
is Extension.Untrusted -> { trustState = it }
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (trustState != null) {
|
||||
ExtensionTrustDialog(
|
||||
onClickConfirm = {
|
||||
onTrustExtension(trustState!!)
|
||||
trustState = null
|
||||
},
|
||||
onClickDismiss = {
|
||||
onUninstallExtension(trustState!!)
|
||||
trustState = null
|
||||
},
|
||||
onDismissRequest = {
|
||||
trustState = null
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ExtensionItem(
|
||||
modifier: Modifier = Modifier,
|
||||
item: ExtensionUiModel.Item,
|
||||
onClickItem: (Extension) -> Unit,
|
||||
onLongClickItem: (Extension) -> Unit,
|
||||
onClickItemCancel: (Extension) -> Unit,
|
||||
onClickItemAction: (Extension) -> Unit,
|
||||
) {
|
||||
val (extension, installStep) = item
|
||||
BaseBrowseItem(
|
||||
modifier = modifier
|
||||
.combinedClickable(
|
||||
onClick = { onClickItem(extension) },
|
||||
onLongClick = { onLongClickItem(extension) },
|
||||
),
|
||||
onClickItem = { onClickItem(extension) },
|
||||
onLongClickItem = { onLongClickItem(extension) },
|
||||
icon = {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(40.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
val idle = installStep.isCompleted()
|
||||
if (!idle) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(40.dp),
|
||||
strokeWidth = 2.dp,
|
||||
)
|
||||
}
|
||||
|
||||
val padding by animateDpAsState(targetValue = if (idle) 0.dp else 8.dp)
|
||||
ExtensionIcon(
|
||||
extension = extension,
|
||||
modifier = Modifier
|
||||
.matchParentSize()
|
||||
.padding(padding),
|
||||
)
|
||||
}
|
||||
},
|
||||
action = {
|
||||
ExtensionItemActions(
|
||||
extension = extension,
|
||||
installStep = installStep,
|
||||
onClickItemCancel = onClickItemCancel,
|
||||
onClickItemAction = onClickItemAction,
|
||||
)
|
||||
},
|
||||
) {
|
||||
ExtensionItemContent(
|
||||
extension = extension,
|
||||
installStep = installStep,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ExtensionItemContent(
|
||||
extension: Extension,
|
||||
installStep: InstallStep,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier.padding(start = MaterialTheme.padding.medium),
|
||||
) {
|
||||
Text(
|
||||
text = extension.name,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
// Won't look good but it's not like we can ellipsize overflowing content
|
||||
FlowRow(
|
||||
modifier = Modifier.secondaryItemAlpha(),
|
||||
mainAxisSpacing = 4.dp,
|
||||
) {
|
||||
ProvideTextStyle(value = MaterialTheme.typography.bodySmall) {
|
||||
if (extension is Extension.Installed && extension.lang.isNotEmpty()) {
|
||||
Text(
|
||||
text = LocaleHelper.getSourceDisplayName(extension.lang, LocalContext.current),
|
||||
)
|
||||
}
|
||||
|
||||
if (extension.versionName.isNotEmpty()) {
|
||||
Text(
|
||||
text = extension.versionName,
|
||||
)
|
||||
}
|
||||
|
||||
val warning = when {
|
||||
extension is Extension.Untrusted -> R.string.ext_untrusted
|
||||
extension is Extension.Installed && extension.isUnofficial -> R.string.ext_unofficial
|
||||
extension is Extension.Installed && extension.isObsolete -> R.string.ext_obsolete
|
||||
extension.isNsfw -> R.string.ext_nsfw_short
|
||||
else -> null
|
||||
}
|
||||
if (warning != null) {
|
||||
Text(
|
||||
text = stringResource(warning).uppercase(),
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
|
||||
if (!installStep.isCompleted()) {
|
||||
DotSeparatorNoSpaceText()
|
||||
Text(
|
||||
text = when (installStep) {
|
||||
InstallStep.Pending -> stringResource(R.string.ext_pending)
|
||||
InstallStep.Downloading -> stringResource(R.string.ext_downloading)
|
||||
InstallStep.Installing -> stringResource(R.string.ext_installing)
|
||||
else -> error("Must not show non-install process text")
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ExtensionItemActions(
|
||||
extension: Extension,
|
||||
installStep: InstallStep,
|
||||
modifier: Modifier = Modifier,
|
||||
onClickItemCancel: (Extension) -> Unit = {},
|
||||
onClickItemAction: (Extension) -> Unit = {},
|
||||
) {
|
||||
val isIdle = installStep.isCompleted()
|
||||
Row(modifier = modifier) {
|
||||
if (isIdle) {
|
||||
TextButton(
|
||||
onClick = { onClickItemAction(extension) },
|
||||
) {
|
||||
Text(
|
||||
text = when (installStep) {
|
||||
InstallStep.Installed -> stringResource(R.string.ext_installed)
|
||||
InstallStep.Error -> stringResource(R.string.action_retry)
|
||||
InstallStep.Idle -> {
|
||||
when (extension) {
|
||||
is Extension.Installed -> {
|
||||
if (extension.hasUpdate) {
|
||||
stringResource(R.string.ext_update)
|
||||
} else {
|
||||
stringResource(R.string.action_settings)
|
||||
}
|
||||
}
|
||||
is Extension.Untrusted -> stringResource(R.string.ext_trust)
|
||||
is Extension.Available -> stringResource(R.string.ext_install)
|
||||
}
|
||||
}
|
||||
else -> error("Must not show install process text")
|
||||
},
|
||||
)
|
||||
}
|
||||
} else {
|
||||
IconButton(onClick = { onClickItemCancel(extension) }) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Close,
|
||||
contentDescription = stringResource(R.string.action_cancel),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ExtensionHeader(
|
||||
@StringRes textRes: Int,
|
||||
modifier: Modifier = Modifier,
|
||||
action: @Composable RowScope.() -> Unit = {},
|
||||
) {
|
||||
ExtensionHeader(
|
||||
text = stringResource(textRes),
|
||||
modifier = modifier,
|
||||
action = action,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ExtensionHeader(
|
||||
text: String,
|
||||
modifier: Modifier = Modifier,
|
||||
action: @Composable RowScope.() -> Unit = {},
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier.padding(horizontal = MaterialTheme.padding.medium),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
modifier = Modifier
|
||||
.padding(vertical = 8.dp)
|
||||
.weight(1f),
|
||||
style = MaterialTheme.typography.header,
|
||||
)
|
||||
action()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ExtensionTrustDialog(
|
||||
onClickConfirm: () -> Unit,
|
||||
onClickDismiss: () -> Unit,
|
||||
onDismissRequest: () -> Unit,
|
||||
) {
|
||||
AlertDialog(
|
||||
title = {
|
||||
Text(text = stringResource(R.string.untrusted_extension))
|
||||
},
|
||||
text = {
|
||||
Text(text = stringResource(R.string.untrusted_extension_message))
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = onClickConfirm) {
|
||||
Text(text = stringResource(R.string.ext_trust))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onClickDismiss) {
|
||||
Text(text = stringResource(R.string.ext_uninstall))
|
||||
}
|
||||
},
|
||||
onDismissRequest = onDismissRequest,
|
||||
)
|
||||
}
|
@ -0,0 +1,112 @@
|
||||
package eu.kanade.presentation.browse
|
||||
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import eu.kanade.presentation.browse.components.GlobalSearchCardRow
|
||||
import eu.kanade.presentation.browse.components.GlobalSearchErrorResultItem
|
||||
import eu.kanade.presentation.browse.components.GlobalSearchLoadingResultItem
|
||||
import eu.kanade.presentation.browse.components.GlobalSearchResultItem
|
||||
import eu.kanade.presentation.browse.components.GlobalSearchToolbar
|
||||
import eu.kanade.presentation.components.LazyColumn
|
||||
import eu.kanade.presentation.components.Scaffold
|
||||
import eu.kanade.presentation.util.padding
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchState
|
||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchItemResult
|
||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
|
||||
@Composable
|
||||
fun GlobalSearchScreen(
|
||||
state: GlobalSearchState,
|
||||
navigateUp: () -> Unit,
|
||||
onChangeSearchQuery: (String?) -> Unit,
|
||||
onSearch: (String) -> Unit,
|
||||
getManga: @Composable (CatalogueSource, Manga) -> State<Manga>,
|
||||
onClickSource: (CatalogueSource) -> Unit,
|
||||
onClickItem: (Manga) -> Unit,
|
||||
onLongClickItem: (Manga) -> Unit,
|
||||
) {
|
||||
Scaffold(
|
||||
topBar = { scrollBehavior ->
|
||||
GlobalSearchToolbar(
|
||||
searchQuery = state.searchQuery,
|
||||
progress = state.progress,
|
||||
total = state.total,
|
||||
navigateUp = navigateUp,
|
||||
onChangeSearchQuery = onChangeSearchQuery,
|
||||
onSearch = onSearch,
|
||||
scrollBehavior = scrollBehavior,
|
||||
)
|
||||
},
|
||||
) { paddingValues ->
|
||||
GlobalSearchContent(
|
||||
items = state.items,
|
||||
contentPadding = paddingValues,
|
||||
getManga = getManga,
|
||||
onClickSource = onClickSource,
|
||||
onClickItem = onClickItem,
|
||||
onLongClickItem = onLongClickItem,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun GlobalSearchContent(
|
||||
items: Map<CatalogueSource, SearchItemResult>,
|
||||
contentPadding: PaddingValues,
|
||||
getManga: @Composable (CatalogueSource, Manga) -> State<Manga>,
|
||||
onClickSource: (CatalogueSource) -> Unit,
|
||||
onClickItem: (Manga) -> Unit,
|
||||
onLongClickItem: (Manga) -> Unit,
|
||||
) {
|
||||
LazyColumn(
|
||||
contentPadding = contentPadding,
|
||||
) {
|
||||
items.forEach { (source, result) ->
|
||||
item(key = source.id) {
|
||||
GlobalSearchResultItem(
|
||||
title = source.name,
|
||||
subtitle = LocaleHelper.getDisplayName(source.lang),
|
||||
onClick = { onClickSource(source) },
|
||||
) {
|
||||
when (result) {
|
||||
SearchItemResult.Loading -> {
|
||||
GlobalSearchLoadingResultItem()
|
||||
}
|
||||
is SearchItemResult.Success -> {
|
||||
if (result.isEmpty) {
|
||||
Text(
|
||||
text = stringResource(R.string.no_results_found),
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
horizontal = MaterialTheme.padding.medium,
|
||||
vertical = MaterialTheme.padding.small,
|
||||
),
|
||||
)
|
||||
return@GlobalSearchResultItem
|
||||
}
|
||||
|
||||
GlobalSearchCardRow(
|
||||
titles = result.result,
|
||||
getManga = { getManga(source, it) },
|
||||
onClick = onClickItem,
|
||||
onLongClick = onLongClickItem,
|
||||
)
|
||||
}
|
||||
is SearchItemResult.Error -> {
|
||||
GlobalSearchErrorResultItem(message = result.throwable.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,84 @@
|
||||
package eu.kanade.presentation.browse
|
||||
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import eu.kanade.presentation.components.AppBar
|
||||
import eu.kanade.presentation.components.EmptyScreen
|
||||
import eu.kanade.presentation.components.FastScrollLazyColumn
|
||||
import eu.kanade.presentation.components.Scaffold
|
||||
import eu.kanade.presentation.manga.components.BaseMangaListItem
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.ui.browse.migration.manga.MigrateMangaState
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
|
||||
@Composable
|
||||
fun MigrateMangaScreen(
|
||||
navigateUp: () -> Unit,
|
||||
title: String?,
|
||||
state: MigrateMangaState,
|
||||
onClickItem: (Manga) -> Unit,
|
||||
onClickCover: (Manga) -> Unit,
|
||||
) {
|
||||
Scaffold(
|
||||
topBar = { scrollBehavior ->
|
||||
AppBar(
|
||||
title = title,
|
||||
navigateUp = navigateUp,
|
||||
scrollBehavior = scrollBehavior,
|
||||
)
|
||||
},
|
||||
) { contentPadding ->
|
||||
if (state.isEmpty) {
|
||||
EmptyScreen(
|
||||
textResource = R.string.empty_screen,
|
||||
modifier = Modifier.padding(contentPadding),
|
||||
)
|
||||
return@Scaffold
|
||||
}
|
||||
|
||||
MigrateMangaContent(
|
||||
contentPadding = contentPadding,
|
||||
state = state,
|
||||
onClickItem = onClickItem,
|
||||
onClickCover = onClickCover,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MigrateMangaContent(
|
||||
contentPadding: PaddingValues,
|
||||
state: MigrateMangaState,
|
||||
onClickItem: (Manga) -> Unit,
|
||||
onClickCover: (Manga) -> Unit,
|
||||
) {
|
||||
FastScrollLazyColumn(
|
||||
contentPadding = contentPadding,
|
||||
) {
|
||||
items(state.titles) { manga ->
|
||||
MigrateMangaItem(
|
||||
manga = manga,
|
||||
onClickItem = onClickItem,
|
||||
onClickCover = onClickCover,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MigrateMangaItem(
|
||||
modifier: Modifier = Modifier,
|
||||
manga: Manga,
|
||||
onClickItem: (Manga) -> Unit,
|
||||
onClickCover: (Manga) -> Unit,
|
||||
) {
|
||||
BaseMangaListItem(
|
||||
modifier = modifier,
|
||||
manga = manga,
|
||||
onClickItem = { onClickItem(manga) },
|
||||
onClickCover = { onClickCover(manga) },
|
||||
)
|
||||
}
|
@ -0,0 +1,101 @@
|
||||
package eu.kanade.presentation.browse
|
||||
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import eu.kanade.presentation.browse.components.GlobalSearchCardRow
|
||||
import eu.kanade.presentation.browse.components.GlobalSearchEmptyResultItem
|
||||
import eu.kanade.presentation.browse.components.GlobalSearchErrorResultItem
|
||||
import eu.kanade.presentation.browse.components.GlobalSearchLoadingResultItem
|
||||
import eu.kanade.presentation.browse.components.GlobalSearchResultItem
|
||||
import eu.kanade.presentation.browse.components.GlobalSearchToolbar
|
||||
import eu.kanade.presentation.components.LazyColumn
|
||||
import eu.kanade.presentation.components.Scaffold
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.ui.browse.migration.search.MigrateSearchState
|
||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchItemResult
|
||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
|
||||
@Composable
|
||||
fun MigrateSearchScreen(
|
||||
navigateUp: () -> Unit,
|
||||
state: MigrateSearchState,
|
||||
getManga: @Composable (CatalogueSource, Manga) -> State<Manga>,
|
||||
onChangeSearchQuery: (String?) -> Unit,
|
||||
onSearch: (String) -> Unit,
|
||||
onClickSource: (CatalogueSource) -> Unit,
|
||||
onClickItem: (Manga) -> Unit,
|
||||
onLongClickItem: (Manga) -> Unit,
|
||||
) {
|
||||
Scaffold(
|
||||
topBar = { scrollBehavior ->
|
||||
GlobalSearchToolbar(
|
||||
searchQuery = state.searchQuery,
|
||||
progress = state.progress,
|
||||
total = state.total,
|
||||
navigateUp = navigateUp,
|
||||
onChangeSearchQuery = onChangeSearchQuery,
|
||||
onSearch = onSearch,
|
||||
scrollBehavior = scrollBehavior,
|
||||
)
|
||||
},
|
||||
) { paddingValues ->
|
||||
MigrateSearchContent(
|
||||
sourceId = state.manga?.source ?: -1,
|
||||
items = state.items,
|
||||
contentPadding = paddingValues,
|
||||
getManga = getManga,
|
||||
onClickSource = onClickSource,
|
||||
onClickItem = onClickItem,
|
||||
onLongClickItem = onLongClickItem,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MigrateSearchContent(
|
||||
sourceId: Long,
|
||||
items: Map<CatalogueSource, SearchItemResult>,
|
||||
contentPadding: PaddingValues,
|
||||
getManga: @Composable (CatalogueSource, Manga) -> State<Manga>,
|
||||
onClickSource: (CatalogueSource) -> Unit,
|
||||
onClickItem: (Manga) -> Unit,
|
||||
onLongClickItem: (Manga) -> Unit,
|
||||
) {
|
||||
LazyColumn(
|
||||
contentPadding = contentPadding,
|
||||
) {
|
||||
items.forEach { (source, result) ->
|
||||
item(key = source.id) {
|
||||
GlobalSearchResultItem(
|
||||
title = if (source.id == sourceId) "▶ ${source.name}" else source.name,
|
||||
subtitle = LocaleHelper.getDisplayName(source.lang),
|
||||
onClick = { onClickSource(source) },
|
||||
) {
|
||||
when (result) {
|
||||
SearchItemResult.Loading -> {
|
||||
GlobalSearchLoadingResultItem()
|
||||
}
|
||||
is SearchItemResult.Success -> {
|
||||
if (result.isEmpty) {
|
||||
GlobalSearchEmptyResultItem()
|
||||
return@GlobalSearchResultItem
|
||||
}
|
||||
|
||||
GlobalSearchCardRow(
|
||||
titles = result.result,
|
||||
getManga = { getManga(source, it) },
|
||||
onClick = onClickItem,
|
||||
onLongClick = onLongClickItem,
|
||||
)
|
||||
}
|
||||
is SearchItemResult.Error -> {
|
||||
GlobalSearchErrorResultItem(message = result.throwable.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,192 @@
|
||||
package eu.kanade.presentation.browse
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.ArrowDownward
|
||||
import androidx.compose.material.icons.outlined.ArrowUpward
|
||||
import androidx.compose.material.icons.outlined.Numbers
|
||||
import androidx.compose.material.icons.outlined.SortByAlpha
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import eu.kanade.domain.source.interactor.SetMigrateSorting
|
||||
import eu.kanade.presentation.browse.components.BaseSourceItem
|
||||
import eu.kanade.presentation.browse.components.SourceIcon
|
||||
import eu.kanade.presentation.components.Badge
|
||||
import eu.kanade.presentation.components.BadgeGroup
|
||||
import eu.kanade.presentation.components.EmptyScreen
|
||||
import eu.kanade.presentation.components.LoadingScreen
|
||||
import eu.kanade.presentation.components.ScrollbarLazyColumn
|
||||
import eu.kanade.presentation.components.Scroller.STICKY_HEADER_KEY_PREFIX
|
||||
import eu.kanade.presentation.theme.header
|
||||
import eu.kanade.presentation.util.padding
|
||||
import eu.kanade.presentation.util.plus
|
||||
import eu.kanade.presentation.util.secondaryItemAlpha
|
||||
import eu.kanade.presentation.util.topSmallPaddingValues
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.ui.browse.migration.sources.MigrateSourceState
|
||||
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
||||
import tachiyomi.domain.source.model.Source
|
||||
|
||||
@Composable
|
||||
fun MigrateSourceScreen(
|
||||
state: MigrateSourceState,
|
||||
contentPadding: PaddingValues,
|
||||
onClickItem: (Source) -> Unit,
|
||||
onToggleSortingDirection: () -> Unit,
|
||||
onToggleSortingMode: () -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
when {
|
||||
state.isLoading -> LoadingScreen(modifier = Modifier.padding(contentPadding))
|
||||
state.isEmpty -> EmptyScreen(
|
||||
textResource = R.string.information_empty_library,
|
||||
modifier = Modifier.padding(contentPadding),
|
||||
)
|
||||
else ->
|
||||
MigrateSourceList(
|
||||
list = state.items,
|
||||
contentPadding = contentPadding,
|
||||
onClickItem = onClickItem,
|
||||
onLongClickItem = { source ->
|
||||
val sourceId = source.id.toString()
|
||||
context.copyToClipboard(sourceId, sourceId)
|
||||
},
|
||||
sortingMode = state.sortingMode,
|
||||
onToggleSortingMode = onToggleSortingMode,
|
||||
sortingDirection = state.sortingDirection,
|
||||
onToggleSortingDirection = onToggleSortingDirection,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MigrateSourceList(
|
||||
list: List<Pair<Source, Long>>,
|
||||
contentPadding: PaddingValues,
|
||||
onClickItem: (Source) -> Unit,
|
||||
onLongClickItem: (Source) -> Unit,
|
||||
sortingMode: SetMigrateSorting.Mode,
|
||||
onToggleSortingMode: () -> Unit,
|
||||
sortingDirection: SetMigrateSorting.Direction,
|
||||
onToggleSortingDirection: () -> Unit,
|
||||
) {
|
||||
ScrollbarLazyColumn(
|
||||
contentPadding = contentPadding + topSmallPaddingValues,
|
||||
) {
|
||||
stickyHeader(key = STICKY_HEADER_KEY_PREFIX) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
.padding(start = MaterialTheme.padding.medium),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.migration_selection_prompt),
|
||||
modifier = Modifier.weight(1f),
|
||||
style = MaterialTheme.typography.header,
|
||||
)
|
||||
|
||||
IconButton(onClick = onToggleSortingMode) {
|
||||
when (sortingMode) {
|
||||
SetMigrateSorting.Mode.ALPHABETICAL -> Icon(Icons.Outlined.SortByAlpha, contentDescription = stringResource(R.string.action_sort_alpha))
|
||||
SetMigrateSorting.Mode.TOTAL -> Icon(Icons.Outlined.Numbers, contentDescription = stringResource(R.string.action_sort_count))
|
||||
}
|
||||
}
|
||||
IconButton(onClick = onToggleSortingDirection) {
|
||||
when (sortingDirection) {
|
||||
SetMigrateSorting.Direction.ASCENDING -> Icon(Icons.Outlined.ArrowUpward, contentDescription = stringResource(R.string.action_asc))
|
||||
SetMigrateSorting.Direction.DESCENDING -> Icon(Icons.Outlined.ArrowDownward, contentDescription = stringResource(R.string.action_desc))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
items(
|
||||
items = list,
|
||||
key = { (source, _) -> "migrate-${source.id}" },
|
||||
) { (source, count) ->
|
||||
MigrateSourceItem(
|
||||
modifier = Modifier.animateItemPlacement(),
|
||||
source = source,
|
||||
count = count,
|
||||
onClickItem = { onClickItem(source) },
|
||||
onLongClickItem = { onLongClickItem(source) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MigrateSourceItem(
|
||||
modifier: Modifier = Modifier,
|
||||
source: Source,
|
||||
count: Long,
|
||||
onClickItem: () -> Unit,
|
||||
onLongClickItem: () -> Unit,
|
||||
) {
|
||||
BaseSourceItem(
|
||||
modifier = modifier,
|
||||
source = source,
|
||||
showLanguageInContent = source.lang != "",
|
||||
onClickItem = onClickItem,
|
||||
onLongClickItem = onLongClickItem,
|
||||
icon = { SourceIcon(source = source) },
|
||||
action = {
|
||||
BadgeGroup {
|
||||
Badge(text = "$count")
|
||||
}
|
||||
},
|
||||
content = { _, sourceLangString ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = MaterialTheme.padding.medium)
|
||||
.weight(1f),
|
||||
) {
|
||||
Text(
|
||||
text = source.name.ifBlank { source.id.toString() },
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
if (sourceLangString != null) {
|
||||
Text(
|
||||
modifier = Modifier.secondaryItemAlpha(),
|
||||
text = sourceLangString,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
}
|
||||
if (source.isStub) {
|
||||
Text(
|
||||
modifier = Modifier.secondaryItemAlpha(),
|
||||
text = stringResource(R.string.not_installed),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user