mirror of
https://github.com/tachiyomiorg/tachiyomi.git
synced 2025-12-15 01:32:06 +01:00
Compare commits
672 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
64c50c1283 | ||
|
|
4146c4c31d | ||
|
|
69223df27c | ||
|
|
4b225a4ff1 | ||
|
|
4a2ee0b596 | ||
|
|
8644d90bd4 | ||
|
|
f30ab56fd0 | ||
|
|
5d91b77c93 | ||
|
|
aca36f9625 | ||
|
|
d5e8c38075 | ||
|
|
6d538db5f2 | ||
|
|
b3d7c92475 | ||
|
|
d862d83511 | ||
|
|
8a1625ec79 | ||
|
|
2ee895ee3c | ||
|
|
7cf2ce2994 | ||
|
|
cb8ea5eab0 | ||
|
|
ce7bf396eb | ||
|
|
1aa5222c99 | ||
|
|
298c49f3ab | ||
|
|
ce5e10be95 | ||
|
|
64ad25d1b5 | ||
|
|
4868dd2d03 | ||
|
|
443d56f69b | ||
|
|
7457a18aee | ||
|
|
d80ba2e807 | ||
|
|
118d3b7fcc | ||
|
|
eed57b80be | ||
|
|
c46c39d4ae | ||
|
|
d7d7a6d2fc | ||
|
|
17b90d2491 | ||
|
|
9ecec5d468 | ||
|
|
0bdd3f79d4 | ||
|
|
7dccde0930 | ||
|
|
c8d68590db | ||
|
|
94448faf97 | ||
|
|
28028c789c | ||
|
|
f8834ee764 | ||
|
|
7c703b17d3 | ||
|
|
f77ade7dda | ||
|
|
91712daee8 | ||
|
|
8057f067b9 | ||
|
|
548f7f415a | ||
|
|
d9c0b1ce7d | ||
|
|
0a0b686119 | ||
|
|
092d930175 | ||
|
|
3b7ed9bc6d | ||
|
|
012854dd1e | ||
|
|
6d1e520c6c | ||
|
|
dcc3141080 | ||
|
|
7326598475 | ||
|
|
fcba2306e9 | ||
|
|
f84868a264 | ||
|
|
15423bfc84 | ||
|
|
8e4cedf173 | ||
|
|
19965e0bdb | ||
|
|
3a35c13575 | ||
|
|
489d22720a | ||
|
|
e1b3345b94 | ||
|
|
c53172265b | ||
|
|
8626a55fe4 | ||
|
|
1302461518 | ||
|
|
8f3681d79f | ||
|
|
c4ce3dd46f | ||
|
|
22df12a680 | ||
|
|
e572abb041 | ||
|
|
ea99d77fda | ||
|
|
2bf77f1d81 | ||
|
|
f79f0a7e97 | ||
|
|
82a9d36df7 | ||
|
|
447bcb28ef | ||
|
|
0be7ac5871 | ||
|
|
d18022c259 | ||
|
|
5619a4c0d9 | ||
|
|
8a7bbfddda | ||
|
|
0026f96fad | ||
|
|
c492efcb31 | ||
|
|
c386d375de | ||
|
|
540fb1bb7c | ||
|
|
81448f5d01 | ||
|
|
7c01201055 | ||
|
|
90d3dd2242 | ||
|
|
97b4d1f13d | ||
|
|
79b37df647 | ||
|
|
b7d282235d | ||
|
|
77ebc362f6 | ||
|
|
8568d5d6c3 | ||
|
|
7ed99fbbd6 | ||
|
|
94cba9324c | ||
|
|
6dab94a937 | ||
|
|
0f42b9f154 | ||
|
|
730f3a6e52 | ||
|
|
72024aa44a | ||
|
|
9c688b08c0 | ||
|
|
c66a4fa7a7 | ||
|
|
e47f4cc177 | ||
|
|
6462472d16 | ||
|
|
78aa50bb35 | ||
|
|
7f0f67d752 | ||
|
|
df332860b8 | ||
|
|
8a8afa46e9 | ||
|
|
509bee0563 | ||
|
|
afb1ee2200 | ||
|
|
66a938779d | ||
|
|
ed506f8495 | ||
|
|
c8e226acb2 | ||
|
|
86edce0d87 | ||
|
|
4e69bf993a | ||
|
|
56d2464870 | ||
|
|
5de72b7d32 | ||
|
|
de92b1351f | ||
|
|
77a8a4229c | ||
|
|
d4290f6f59 | ||
|
|
b08d604d2a | ||
|
|
9e04f14a7b | ||
|
|
6663abebaf | ||
|
|
e5f83d0c6e | ||
|
|
3ad7add3b5 | ||
|
|
66aacade9a | ||
|
|
fe3a710ed0 | ||
|
|
f9754f4f58 | ||
|
|
8824c7dbe3 | ||
|
|
ccc9a5a052 | ||
|
|
f5e0cee36c | ||
|
|
36f1e0e476 | ||
|
|
2dd2db7225 | ||
|
|
3d0e750519 | ||
|
|
26c5d761da | ||
|
|
1668be8587 | ||
|
|
86a3fc77c6 | ||
|
|
cc018cee18 | ||
|
|
d9d143e6be | ||
|
|
3f0db60a99 | ||
|
|
87f3d4bd05 | ||
|
|
5c3d655d9e | ||
|
|
66b175a3c8 | ||
|
|
3cd3f45c8a | ||
|
|
816d7815e9 | ||
|
|
dbc7fe4d54 | ||
|
|
d29b7c4e57 | ||
|
|
772db51593 | ||
|
|
87530f506e | ||
|
|
98d6ce2eaf | ||
|
|
7644d7c31e | ||
|
|
dde2f42138 | ||
|
|
6922792ad1 | ||
|
|
f32243899d | ||
|
|
13dc54df70 | ||
|
|
6d9a8a30e9 | ||
|
|
2bf263e301 | ||
|
|
c06beac660 | ||
|
|
74f74eef56 | ||
|
|
3aafec482c | ||
|
|
ed80ac3154 | ||
|
|
eeeaae4570 | ||
|
|
d1c956401c | ||
|
|
1be7949275 | ||
|
|
5572b28d01 | ||
|
|
4e68b62881 | ||
|
|
4e31e6a2fa | ||
|
|
bc692ebfc6 | ||
|
|
96f6a5abc2 | ||
|
|
3411ac40c0 | ||
|
|
8a6a104987 | ||
|
|
efa7a3a167 | ||
|
|
67bc81ebde | ||
|
|
0a3ce8ebe4 | ||
|
|
3ebf39bd55 | ||
|
|
8f395d98e7 | ||
|
|
26b3eb696c | ||
|
|
627f07408e | ||
|
|
7146913c71 | ||
|
|
39c6bcccd8 | ||
|
|
6259bbaa5e | ||
|
|
cb4b8ac0dc | ||
|
|
4b7acdb022 | ||
|
|
af0fdfa3b7 | ||
|
|
d874f20362 | ||
|
|
8680accd8e | ||
|
|
7308090288 | ||
|
|
400ca48456 | ||
|
|
9b6567f5e4 | ||
|
|
7798186c32 | ||
|
|
9dc66c7c8d | ||
|
|
10b0ef9b6d | ||
|
|
81cd765543 | ||
|
|
c9a1bd86b5 | ||
|
|
dfbbbadfac | ||
|
|
0f21d16263 | ||
|
|
d65f9c2916 | ||
|
|
5718983f41 | ||
|
|
f7b335e4fb | ||
|
|
aa6937baf2 | ||
|
|
59f7d2273f | ||
|
|
cd91ea9b77 | ||
|
|
6a558ad119 | ||
|
|
f5936e9456 | ||
|
|
9df351da0a | ||
|
|
90325d48aa | ||
|
|
e2abf283fe | ||
|
|
db788d519d | ||
|
|
f3e9d5f346 | ||
|
|
fd30c0adcd | ||
|
|
3ad4f1114a | ||
|
|
fe90546821 | ||
|
|
3892c4caac | ||
|
|
cb639f4e90 | ||
|
|
6d69caf59e | ||
|
|
cdc1c5efa3 | ||
|
|
77bfd0c099 | ||
|
|
8ff0c9d61a | ||
|
|
b6620434b3 | ||
|
|
abae9bf37d | ||
|
|
2556e9f08c | ||
|
|
7aa172c512 | ||
|
|
81cf232bcb | ||
|
|
ee26d6dffd | ||
|
|
7b2764e8f7 | ||
|
|
46e3b9e40d | ||
|
|
8d00ff1b40 | ||
|
|
cf14831fbe | ||
|
|
7a4680603d | ||
|
|
99f12b1fbf | ||
|
|
5c73045aa4 | ||
|
|
ac306547a0 | ||
|
|
3f868c0435 | ||
|
|
262ce3473f | ||
|
|
43b9b104f5 | ||
|
|
c7f0a54a37 | ||
|
|
ca789dca0e | ||
|
|
ef7b285151 | ||
|
|
dd3ca0c131 | ||
|
|
8b46e8edad | ||
|
|
30f845139d | ||
|
|
818471b7e1 | ||
|
|
a3a3f44056 | ||
|
|
22c6dbda3f | ||
|
|
34f7caa0fc | ||
|
|
01553b1ed8 | ||
|
|
a24afa9a76 | ||
|
|
ec08ba05fc | ||
|
|
30bea8b753 | ||
|
|
09e4b5a9cd | ||
|
|
5467104b95 | ||
|
|
e0733c1a4c | ||
|
|
1cf7f9be54 | ||
|
|
fb99577836 | ||
|
|
28131ac135 | ||
|
|
e40b8d537c | ||
|
|
12e7ee9d0c | ||
|
|
54733e6ceb | ||
|
|
22e8050fff | ||
|
|
a629db2884 | ||
|
|
cbcec8c4d9 | ||
|
|
2f05f7b91f | ||
|
|
a3a9699e8a | ||
|
|
f01a312c23 | ||
|
|
0fffde50ff | ||
|
|
8775596a82 | ||
|
|
2f0133986a | ||
|
|
3bd2cad45f | ||
|
|
48f7a2de41 | ||
|
|
3aa6e7ae0e | ||
|
|
813d7e49cd | ||
|
|
710ebfb7a5 | ||
|
|
87bdee5990 | ||
|
|
efabe801be | ||
|
|
9a817e49be | ||
|
|
a577f5534f | ||
|
|
d0f52ea93d | ||
|
|
6063efd101 | ||
|
|
0759936226 | ||
|
|
1e3d9a00f2 | ||
|
|
7c62453280 | ||
|
|
226272f686 | ||
|
|
16cbcecd99 | ||
|
|
b008223661 | ||
|
|
f8cf3db4a4 | ||
|
|
a585d46e7a | ||
|
|
8cc42bce5a | ||
|
|
67c6dbea0d | ||
|
|
db33437577 | ||
|
|
8287c9d193 | ||
|
|
d32409bd6e | ||
|
|
cf3f2d0380 | ||
|
|
53c6230afe | ||
|
|
4882896f4d | ||
|
|
4d67066de3 | ||
|
|
6fe5e6e21b | ||
|
|
8c5496b53f | ||
|
|
235a587e42 | ||
|
|
3125d78706 | ||
|
|
bb8f3c63f1 | ||
|
|
20faaaa908 | ||
|
|
44cc6f11c7 | ||
|
|
bae391c2c1 | ||
|
|
0ac5f3b93c | ||
|
|
b79ef5dc79 | ||
|
|
7d26ca046f | ||
|
|
d99f4697e8 | ||
|
|
bb3fdef40b | ||
|
|
2a7cca6ea4 | ||
|
|
6ed2748846 | ||
|
|
7c90fe0f7d | ||
|
|
8a5e443ca5 | ||
|
|
a07e0df815 | ||
|
|
88e9fefa59 | ||
|
|
c0fd47b066 | ||
|
|
ee684cbef5 | ||
|
|
1f618d6634 | ||
|
|
7d4af1f8cc | ||
|
|
fe82cdb9c8 | ||
|
|
b354e37cc3 | ||
|
|
f344831d58 | ||
|
|
2eca8511cb | ||
|
|
f2b0d74b4c | ||
|
|
42bc2b07ce | ||
|
|
e2d6269a38 | ||
|
|
fcfa62f220 | ||
|
|
25b0458930 | ||
|
|
6808fbbb21 | ||
|
|
b36b3bfcab | ||
|
|
7f0ed58b54 | ||
|
|
b4393ff741 | ||
|
|
b8af1621b5 | ||
|
|
4a75f82a6f | ||
|
|
740e370465 | ||
|
|
245985bf42 | ||
|
|
344f5afd50 | ||
|
|
1c6e5605f9 | ||
|
|
0871208023 | ||
|
|
ee95c1439f | ||
|
|
e323f3c25a | ||
|
|
9766399539 | ||
|
|
fc4fd487f9 | ||
|
|
dddba7bb6f | ||
|
|
9ec8d770ea | ||
|
|
cf777d9893 | ||
|
|
0d9f8e8743 | ||
|
|
841f80f935 | ||
|
|
39a7356ed1 | ||
|
|
438054a0ec | ||
|
|
34b9c82cd0 | ||
|
|
405a75438a | ||
|
|
39e4568460 | ||
|
|
0d96791a84 | ||
|
|
531e1c62bb | ||
|
|
1a1f16f44a | ||
|
|
431f8772f8 | ||
|
|
8a5382042c | ||
|
|
8f4bc71cf7 | ||
|
|
0ac38297f4 | ||
|
|
4c65c2311e | ||
|
|
f48f212001 | ||
|
|
c90f344910 | ||
|
|
a458bd9fdb | ||
|
|
ed5a56be60 | ||
|
|
899fe57f15 | ||
|
|
bac42edabb | ||
|
|
8735f3566f | ||
|
|
46efd4c134 | ||
|
|
dfd38db7e3 | ||
|
|
0189fc1f66 | ||
|
|
929a881943 | ||
|
|
152fdec855 | ||
|
|
9c07451d95 | ||
|
|
e3b2720924 | ||
|
|
3ae1e37c40 | ||
|
|
d8998aacb4 | ||
|
|
efdff9a21a | ||
|
|
1824adb2ed | ||
|
|
38445673f3 | ||
|
|
5a9889b562 | ||
|
|
5ca7c39751 | ||
|
|
44609c494c | ||
|
|
0810d3db69 | ||
|
|
d4fb9995ef | ||
|
|
22a4372583 | ||
|
|
a4d86a2e1e | ||
|
|
b8716ff6fe | ||
|
|
73118d4af7 | ||
|
|
c27bf4e866 | ||
|
|
f50f5c4b54 | ||
|
|
fb38d30775 | ||
|
|
a3a9c8ac8e | ||
|
|
b4bb855675 | ||
|
|
6263a52777 | ||
|
|
96defd6b05 | ||
|
|
8df9bce1b4 | ||
|
|
bcd90be525 | ||
|
|
22afae4449 | ||
|
|
8fae92034e | ||
|
|
ce81b76150 | ||
|
|
f70d5ea976 | ||
|
|
dbbf6c5de0 | ||
|
|
2379df7e60 | ||
|
|
e3ce3ff418 | ||
|
|
4395202703 | ||
|
|
84acae27b7 | ||
|
|
71f6e07e71 | ||
|
|
6f59c6c6bb | ||
|
|
d36cf5ce15 | ||
|
|
332d9ff61b | ||
|
|
7bb1ccf6f7 | ||
|
|
05a7d5174a | ||
|
|
b051e37ab7 | ||
|
|
44383ff950 | ||
|
|
1b25290d39 | ||
|
|
2f5eb73d29 | ||
|
|
f0dd33ee4c | ||
|
|
e15b945e16 | ||
|
|
5c7d88c2ed | ||
|
|
bbe0ab1dd0 | ||
|
|
cb2d43c0d1 | ||
|
|
fce9cb820c | ||
|
|
08e4863d94 | ||
|
|
9a10656bf0 | ||
|
|
3c79777e66 | ||
|
|
f5ad95d78a | ||
|
|
14c465d36f | ||
|
|
921a988c4a | ||
|
|
99378ddf20 | ||
|
|
c623258e8c | ||
|
|
6ce42dc167 | ||
|
|
f63573f25f | ||
|
|
b328f0e344 | ||
|
|
02864ebd60 | ||
|
|
eed91f6360 | ||
|
|
f317193bec | ||
|
|
f459515dd7 | ||
|
|
9339ea4196 | ||
|
|
6bdc1b676e | ||
|
|
7451c13edd | ||
|
|
ccd4143d9d | ||
|
|
c590f55030 | ||
|
|
c21813a8b5 | ||
|
|
2cb08e6bb1 | ||
|
|
058ee4c86b | ||
|
|
ea6e5eebac | ||
|
|
9cc25ff345 | ||
|
|
c9805b8612 | ||
|
|
41c89eb61d | ||
|
|
392c3492b3 | ||
|
|
f7cd3929a3 | ||
|
|
20bec66a9d | ||
|
|
3ce9a9ff97 | ||
|
|
a8f17a3fab | ||
|
|
ef3d2c14b4 | ||
|
|
44619febd3 | ||
|
|
c48accb357 | ||
|
|
418e6a8b3a | ||
|
|
67b4e53a58 | ||
|
|
d62d94f587 | ||
|
|
265934d77a | ||
|
|
2a218cca90 | ||
|
|
e23cc8f83a | ||
|
|
0b125b7106 | ||
|
|
320587e36e | ||
|
|
26f3995595 | ||
|
|
94c94b2d88 | ||
|
|
41cc1fe723 | ||
|
|
03a344e9c1 | ||
|
|
add228407f | ||
|
|
2c6e025063 | ||
|
|
ba30dfe7e2 | ||
|
|
97e6f1ea9a | ||
|
|
5c1a81d8ca | ||
|
|
c615f4d458 | ||
|
|
9e09a20e65 | ||
|
|
7115a9b9fe | ||
|
|
fd8b97fc87 | ||
|
|
4dd67e4348 | ||
|
|
10973bf3cd | ||
|
|
934ed0551a | ||
|
|
38428c6ebe | ||
|
|
bf85e147e7 | ||
|
|
d2dd34c2e5 | ||
|
|
c4ab2b4675 | ||
|
|
aa2ec5940f | ||
|
|
79323de326 | ||
|
|
08e6487a9a | ||
|
|
4498b10a10 | ||
|
|
6f2bb18d72 | ||
|
|
7e56cba060 | ||
|
|
dc569fb20a | ||
|
|
c6ac992798 | ||
|
|
8a18e10cc2 | ||
|
|
d6b9711e45 | ||
|
|
8ab7e63293 | ||
|
|
4bcd623829 | ||
|
|
18acf66cb8 | ||
|
|
4816b4b53a | ||
|
|
60d8650860 | ||
|
|
bfb7b5afd5 | ||
|
|
a2627d70af | ||
|
|
6662a97b2f | ||
|
|
564a0980b9 | ||
|
|
e3fbd26880 | ||
|
|
c1e23ec18e | ||
|
|
b7cd7b8b4e | ||
|
|
776d36caf1 | ||
|
|
182e642cfc | ||
|
|
88bf1a706b | ||
|
|
d25ba23079 | ||
|
|
75460e01c8 | ||
|
|
c9bd3a5314 | ||
|
|
7c6a5dc43b | ||
|
|
274218cf22 | ||
|
|
c7d6509565 | ||
|
|
bc0b9e536a | ||
|
|
7a1b599462 | ||
|
|
1dd62af188 | ||
|
|
6f1099b710 | ||
|
|
be8e2f119f | ||
|
|
18f9e5ba6b | ||
|
|
d1bf857079 | ||
|
|
1814b3b22c | ||
|
|
be54b8862e | ||
|
|
1a61130f0b | ||
|
|
1de4bc9586 | ||
|
|
1986042277 | ||
|
|
e932983494 | ||
|
|
35d381144d | ||
|
|
0bcc22822d | ||
|
|
1ff78173f7 | ||
|
|
ee45f46193 | ||
|
|
290efb0283 | ||
|
|
8d7a7919a9 | ||
|
|
953720472f | ||
|
|
f94d902bb6 | ||
|
|
da25322572 | ||
|
|
cb4699a5bb | ||
|
|
2e5efadf42 | ||
|
|
e5e18c2030 | ||
|
|
ac0596a53d | ||
|
|
7ec5a51eb8 | ||
|
|
3cca460282 | ||
|
|
d703fb7946 | ||
|
|
859601a46e | ||
|
|
cdc160afc2 | ||
|
|
14d1bcacc9 | ||
|
|
abd23b6826 | ||
|
|
7d8a865cac | ||
|
|
dfdb688b43 | ||
|
|
c955ac6a66 | ||
|
|
f3ca4e76a8 | ||
|
|
2769525b2c | ||
|
|
843e748de3 | ||
|
|
d160cfaa0e | ||
|
|
81af97df77 | ||
|
|
18e55aa25f | ||
|
|
4d3e13b0d1 | ||
|
|
a335b4ee9e | ||
|
|
47a2d06682 | ||
|
|
ce66ed0389 | ||
|
|
c0f94ae8af | ||
|
|
ed32a511e7 | ||
|
|
17ed4873e8 | ||
|
|
09acc53483 | ||
|
|
bebd4be43d | ||
|
|
9b77759f24 | ||
|
|
e458de5e9c | ||
|
|
737a303df7 | ||
|
|
477dd37981 | ||
|
|
e917349bb7 | ||
|
|
ad4912803b | ||
|
|
f96f0c5889 | ||
|
|
2b9acadc5b | ||
|
|
9caa0d147b | ||
|
|
c6e5f8abd9 | ||
|
|
1abf01c4a0 | ||
|
|
b41565f879 | ||
|
|
f03a834136 | ||
|
|
f7f2072621 | ||
|
|
5b2e937d5f | ||
|
|
f27dc19b37 | ||
|
|
2368c50ebb | ||
|
|
0505906e7a | ||
|
|
4efca04765 | ||
|
|
b12c7cf963 | ||
|
|
487622c592 | ||
|
|
26d422b0ae | ||
|
|
79a7b68837 | ||
|
|
63048d2f0b | ||
|
|
79662a5866 | ||
|
|
ed6809fa28 | ||
|
|
86b9262a7e | ||
|
|
7ec87e76db | ||
|
|
a0e76d2fd9 | ||
|
|
ec3ce74af8 | ||
|
|
83a4e34095 | ||
|
|
84a0044d51 | ||
|
|
92132c59f5 | ||
|
|
36ae388332 | ||
|
|
bd47eafeec | ||
|
|
9432d2d06a | ||
|
|
fa61c8fe6f | ||
|
|
92bd98e45f | ||
|
|
fd7c993b0b | ||
|
|
779df32e98 | ||
|
|
f4e843f114 | ||
|
|
c0e2eb211d | ||
|
|
0bd56ab77c | ||
|
|
6b03dca5f4 | ||
|
|
bd7b21337c | ||
|
|
60a3ba5a5c | ||
|
|
7c2eb0b881 | ||
|
|
93523ef50b | ||
|
|
10d7349506 | ||
|
|
3d7c136320 | ||
|
|
a6d6a5ed87 | ||
|
|
b690de55e5 | ||
|
|
83fda20078 | ||
|
|
f656a37045 | ||
|
|
c58b495433 | ||
|
|
242aeb6a68 | ||
|
|
d9969cea8a | ||
|
|
d61db5931e | ||
|
|
0ea3ac9807 | ||
|
|
f9e43f574f | ||
|
|
5ef11e61d0 | ||
|
|
48546c3db4 | ||
|
|
4d87ed496c | ||
|
|
06d12e6562 | ||
|
|
ec49411bee | ||
|
|
3f7911235c | ||
|
|
727399611d | ||
|
|
94232a4937 | ||
|
|
07fdb74fbc | ||
|
|
d400ac2a49 | ||
|
|
dd71c76a8f | ||
|
|
58a0add4f6 | ||
|
|
bfe143015a | ||
|
|
e3cf863230 | ||
|
|
ee818bc7c5 | ||
|
|
f816196df2 | ||
|
|
753bf7de5d | ||
|
|
3634b52e3a | ||
|
|
ef863335e6 | ||
|
|
ceaf579cb0 | ||
|
|
b49280e347 | ||
|
|
d3dadf71e8 | ||
|
|
ffa8c8fd07 | ||
|
|
0ef7650c1a | ||
|
|
4635e58405 | ||
|
|
dc2eaf0788 | ||
|
|
d02b0ca2db | ||
|
|
4d607c4aed | ||
|
|
be4072c86b | ||
|
|
2970eca9e4 | ||
|
|
42954609b9 | ||
|
|
6348cbaeb7 | ||
|
|
a7cb33d8c9 | ||
|
|
ec46b2281b | ||
|
|
3a2dc46ff0 | ||
|
|
e052bdef96 | ||
|
|
d522d6d545 | ||
|
|
7b118eba22 | ||
|
|
f6e6a7ddf1 | ||
|
|
5ce64ac7ff | ||
|
|
1671a56f42 | ||
|
|
ab6dfe9e25 | ||
|
|
bff98ca768 | ||
|
|
32b9b261f0 | ||
|
|
23432e4405 | ||
|
|
34a586ce48 | ||
|
|
ad762f8303 | ||
|
|
389b039679 | ||
|
|
ef9dacde79 | ||
|
|
13bb45b4be | ||
|
|
bd2cb97179 | ||
|
|
0d8f1c8560 |
@@ -3,5 +3,5 @@ indent_size=4
|
|||||||
insert_final_newline=true
|
insert_final_newline=true
|
||||||
ij_kotlin_allow_trailing_comma=true
|
ij_kotlin_allow_trailing_comma=true
|
||||||
ij_kotlin_allow_trailing_comma_on_call_site=true
|
ij_kotlin_allow_trailing_comma_on_call_site=true
|
||||||
ij_kotlin_name_count_to_use_star_import = 2147483647
|
ij_kotlin_name_count_to_use_star_import=2147483647
|
||||||
ij_kotlin_name_count_to_use_star_import_for_members = 2147483647
|
ij_kotlin_name_count_to_use_star_import_for_members=2147483647
|
||||||
|
|||||||
4
.github/ISSUE_TEMPLATE.md
vendored
4
.github/ISSUE_TEMPLATE.md
vendored
@@ -3,9 +3,9 @@
|
|||||||
I acknowledge that:
|
I acknowledge that:
|
||||||
|
|
||||||
- I have updated:
|
- I have updated:
|
||||||
- To the latest version of the app (stable is v0.14.4)
|
- To the latest version of the app (stable is v0.14.7)
|
||||||
- All extensions
|
- All extensions
|
||||||
- I have tried the troubleshooting guide: https://tachiyomi.org/help/guides/troubleshooting-problems/
|
- I have gone through the FAQ (https://tachiyomi.org/docs/faq/general) and troubleshooting guide (https://tachiyomi.org/docs/guides/troubleshooting/)
|
||||||
- If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions
|
- If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions
|
||||||
- I have searched the existing issues and this is new ticket **NOT** a duplicate or related to another open or closed 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
|
- I will fill out the title and the information in this template
|
||||||
|
|||||||
4
.github/ISSUE_TEMPLATE/config.yml
vendored
4
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -4,8 +4,8 @@ contact_links:
|
|||||||
url: https://github.com/tachiyomiorg/tachiyomi-extensions/issues/new/choose
|
url: https://github.com/tachiyomiorg/tachiyomi-extensions/issues/new/choose
|
||||||
about: Issues and requests for extensions and sources should be opened in the tachiyomi-extensions repository instead
|
about: Issues and requests for extensions and sources should be opened in the tachiyomi-extensions repository instead
|
||||||
- name: 📦 Tachiyomi extensions
|
- name: 📦 Tachiyomi extensions
|
||||||
url: https://tachiyomi.org/extensions
|
url: https://tachiyomi.org/extensions/
|
||||||
about: List of all available extensions with download links
|
about: List of all available extensions with download links
|
||||||
- name: 🖥️ Tachiyomi website
|
- name: 🖥️ Tachiyomi website
|
||||||
url: https://tachiyomi.org/help/
|
url: https://tachiyomi.org/
|
||||||
about: Guides, troubleshooting, and answers to common questions
|
about: Guides, troubleshooting, and answers to common questions
|
||||||
|
|||||||
6
.github/ISSUE_TEMPLATE/report_issue.yml
vendored
6
.github/ISSUE_TEMPLATE/report_issue.yml
vendored
@@ -53,7 +53,7 @@ body:
|
|||||||
label: Tachiyomi version
|
label: Tachiyomi version
|
||||||
description: You can find your Tachiyomi version in **More → About**.
|
description: You can find your Tachiyomi version in **More → About**.
|
||||||
placeholder: |
|
placeholder: |
|
||||||
Example: "0.14.4"
|
Example: "0.14.7"
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
@@ -96,9 +96,9 @@ body:
|
|||||||
required: true
|
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).
|
- 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
|
required: true
|
||||||
- label: I have tried the [troubleshooting guide](https://tachiyomi.org/help/guides/troubleshooting/).
|
- label: I have gone through the [FAQ](https://tachiyomi.org/docs/faq/general) and [troubleshooting guide](https://tachiyomi.org/docs/guides/troubleshooting/).
|
||||||
required: true
|
required: true
|
||||||
- label: I have updated the app to version **[0.14.4](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
|
required: true
|
||||||
- label: I have updated all installed extensions.
|
- label: I have updated all installed extensions.
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
2
.github/ISSUE_TEMPLATE/request_feature.yml
vendored
2
.github/ISSUE_TEMPLATE/request_feature.yml
vendored
@@ -33,7 +33,7 @@ body:
|
|||||||
required: true
|
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).
|
- 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
|
required: true
|
||||||
- label: I have updated the app to version **[0.14.4](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
|
required: true
|
||||||
- label: I will fill out all of the requested information in this form.
|
- label: I will fill out all of the requested information in this form.
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
12
.github/renovate.json
vendored
12
.github/renovate.json
vendored
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": [
|
|
||||||
"config:base"
|
|
||||||
],
|
|
||||||
"schedule": ["every sunday"],
|
|
||||||
"ignoreDeps": [
|
|
||||||
"androidx.core:core-splashscreen",
|
|
||||||
"com.android.tools:r8",
|
|
||||||
"com.google.guava:guava",
|
|
||||||
"com.github.commandiron:WheelPickerCompose"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
17
.github/renovate.json5
vendored
Normal file
17
.github/renovate.json5
vendored
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||||
|
"extends": [
|
||||||
|
"config:base"
|
||||||
|
],
|
||||||
|
"schedule": ["every sunday"],
|
||||||
|
"packageRules": [
|
||||||
|
{
|
||||||
|
// Compiler plugins are tightly coupled to Kotlin version
|
||||||
|
"groupName": "Kotlin",
|
||||||
|
"matchPackagePrefixes": [
|
||||||
|
"androidx.compose.compiler",
|
||||||
|
"org.jetbrains.kotlin",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
8
.github/workflows/build_pull_request.yml
vendored
8
.github/workflows/build_pull_request.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Clone repo
|
- name: Clone repo
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Validate Gradle Wrapper
|
- name: Validate Gradle Wrapper
|
||||||
uses: gradle/wrapper-validation-action@v1
|
uses: gradle/wrapper-validation-action@v1
|
||||||
@@ -27,13 +27,13 @@ jobs:
|
|||||||
- name: Dependency Review
|
- name: Dependency Review
|
||||||
uses: actions/dependency-review-action@v3
|
uses: actions/dependency-review-action@v3
|
||||||
|
|
||||||
- name: Set up JDK 11
|
- name: Set up JDK
|
||||||
uses: actions/setup-java@v3
|
uses: actions/setup-java@v3
|
||||||
with:
|
with:
|
||||||
java-version: 11
|
java-version: 17
|
||||||
distribution: adopt
|
distribution: adopt
|
||||||
|
|
||||||
- name: Build app and run unit tests
|
- name: Build app and run unit tests
|
||||||
uses: gradle/gradle-command-action@v2
|
uses: gradle/gradle-command-action@v2
|
||||||
with:
|
with:
|
||||||
arguments: lintKotlin assembleStandardRelease testStandardReleaseUnitTest
|
arguments: ktlintCheck assembleStandardRelease testReleaseUnitTest
|
||||||
18
.github/workflows/build_push.yml
vendored
18
.github/workflows/build_push.yml
vendored
@@ -17,21 +17,21 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Clone repo
|
- name: Clone repo
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Validate Gradle Wrapper
|
- name: Validate Gradle Wrapper
|
||||||
uses: gradle/wrapper-validation-action@v1
|
uses: gradle/wrapper-validation-action@v1
|
||||||
|
|
||||||
- name: Set up JDK 11
|
- name: Set up JDK
|
||||||
uses: actions/setup-java@v3
|
uses: actions/setup-java@v3
|
||||||
with:
|
with:
|
||||||
java-version: 11
|
java-version: 17
|
||||||
distribution: adopt
|
distribution: adopt
|
||||||
|
|
||||||
- name: Build app and run unit tests
|
- name: Build app and run unit tests
|
||||||
uses: gradle/gradle-command-action@v2
|
uses: gradle/gradle-command-action@v2
|
||||||
with:
|
with:
|
||||||
arguments: lintKotlin assembleStandardRelease testStandardReleaseUnitTest
|
arguments: ktlintCheck assembleStandardRelease testReleaseUnitTest
|
||||||
|
|
||||||
# Sign APK and create release for tags
|
# Sign APK and create release for tags
|
||||||
|
|
||||||
@@ -104,3 +104,13 @@ jobs:
|
|||||||
prerelease: false
|
prerelease: false
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
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 }}
|
||||||
|
|||||||
12
.github/workflows/issue_moderator.yml
vendored
12
.github/workflows/issue_moderator.yml
vendored
@@ -11,9 +11,11 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Moderate issues
|
- name: Moderate issues
|
||||||
uses: tachiyomiorg/issue-moderator-action@v1
|
uses: tachiyomiorg/issue-moderator-action@v2
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
duplicate-label: Duplicate
|
||||||
|
|
||||||
auto-close-rules: |
|
auto-close-rules: |
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
@@ -31,5 +33,13 @@ jobs:
|
|||||||
"regex": "^(?!.*myanimelist.*).*(aniyomi|anime).*$",
|
"regex": "^(?!.*myanimelist.*).*(aniyomi|anime).*$",
|
||||||
"ignoreCase": true,
|
"ignoreCase": true,
|
||||||
"message": "Tachiyomi does not support anime, and has no plans to support anime. In addition Tachiyomi is not affiliated with Aniyomi https://github.com/jmir1/aniyomi"
|
"message": "Tachiyomi does not support anime, and has no plans to support anime. In addition Tachiyomi is not affiliated with Aniyomi https://github.com/jmir1/aniyomi"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "both",
|
||||||
|
"regex": ".*(?:fail(?:ed|ure|s)?|can\\s*(?:no|')?t|(?:not|un).*able|(?<!n[o']?t )blocked by|error) (?:to )?(?:get past|by ?pass|penetrate)?.*cloud ?fl?are.*",
|
||||||
|
"ignoreCase": true,
|
||||||
|
"labels": ["Cloudflare protected"],
|
||||||
|
"message": "Refer to the **Solving Cloudflare issues** section at https://tachiyomi.org/docs/guides/troubleshooting/#cloudflare. If it doesn't work, migrate to other sources or wait until they lower their protection."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
auto-close-ignore-label: do-not-autoclose
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -2,7 +2,8 @@
|
|||||||
/local.properties
|
/local.properties
|
||||||
/.idea/workspace.xml
|
/.idea/workspace.xml
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.idea/
|
.idea/*
|
||||||
|
!.idea/icon.png
|
||||||
*iml
|
*iml
|
||||||
*.iml
|
*.iml
|
||||||
|
|
||||||
|
|||||||
BIN
.idea/icon.png
generated
Normal file
BIN
.idea/icon.png
generated
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
@@ -24,13 +24,17 @@ Before you start, please note that the ability to use following technologies is
|
|||||||
- [Android Studio](https://developer.android.com/studio)
|
- [Android Studio](https://developer.android.com/studio)
|
||||||
- Emulator or phone with developer options enabled to test changes.
|
- Emulator or phone with developer options enabled to test changes.
|
||||||
|
|
||||||
|
## Linting
|
||||||
|
|
||||||
|
To auto-fix some linting errors, run the `ktlintFormat` Gradle task.
|
||||||
|
|
||||||
## Getting help
|
## Getting help
|
||||||
|
|
||||||
- Join [the Discord server](https://discord.gg/tachiyomi) for online help and to ask questions while developing.
|
- Join [the Discord server](https://discord.gg/tachiyomi) for online help and to ask questions while developing.
|
||||||
|
|
||||||
# Translations
|
# Translations
|
||||||
|
|
||||||
Translations are done externally via Weblate. See [our website](https://tachiyomi.org/help/contribution/#translation) for more details.
|
Translations are done externally via Weblate. See [our website](https://tachiyomi.org/docs/contribute#translation) for more details.
|
||||||
|
|
||||||
|
|
||||||
# Forks
|
# Forks
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
| Build | Stable | Weekly Preview | Contribute | Support Server |
|
| Build | Stable | Weekly Preview | Contribute | Support Server |
|
||||||
|-------|----------|---------|------------|---------|
|
|-------|----------|---------|------------|---------|
|
||||||
|  | [](https://github.com/tachiyomiorg/tachiyomi/releases) | [](https://github.com/tachiyomiorg/tachiyomi-preview/releases) | [](https://hosted.weblate.org/engage/tachiyomi/?utm_source=widget) | [](https://discord.gg/tachiyomi) |
|
| [](https://github.com/tachiyomiorg/tachiyomi/actions/workflows/build_push.yml) | [](https://github.com/tachiyomiorg/tachiyomi/releases) | [](https://github.com/tachiyomiorg/tachiyomi-preview/releases) | [](https://hosted.weblate.org/engage/tachiyomi/?utm_source=widget) | [](https://discord.gg/tachiyomi) |
|
||||||
|
|
||||||
|
|
||||||
# Tachiyomi
|
# Tachiyomi
|
||||||
@@ -29,7 +29,7 @@ Please make sure to read the full guidelines. Your issue may be closed without w
|
|||||||
|
|
||||||
<details><summary>Issues</summary>
|
<details><summary>Issues</summary>
|
||||||
|
|
||||||
1. **Before reporting a new issue, take a look at the [FAQ](https://tachiyomi.org/help/faq/), the [changelog](https://github.com/tachiyomiorg/tachiyomi/releases) and the already opened [issues](https://github.com/tachiyomiorg/tachiyomi/issues).**
|
1. **Before reporting a new issue, take a look at the [FAQ](https://tachiyomi.org/docs/faq/general), the [changelog](https://tachiyomi.org/changelogs/) and the already opened [issues](https://github.com/tachiyomiorg/tachiyomi/issues).**
|
||||||
2. If you are unsure, ask here: [](https://discord.gg/tachiyomi)
|
2. If you are unsure, ask here: [](https://discord.gg/tachiyomi)
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
@@ -38,7 +38,7 @@ Please make sure to read the full guidelines. Your issue may be closed without w
|
|||||||
|
|
||||||
* Include version (More → About → Version)
|
* Include version (More → About → Version)
|
||||||
* If not latest, try updating, it may have already been solved
|
* If not latest, try updating, it may have already been solved
|
||||||
* Preview version is equal to the number of commits as seen in the main page
|
* Preview version is equal to the number of commits as seen on the main page
|
||||||
* Include steps to reproduce (if not obvious from description)
|
* Include steps to reproduce (if not obvious from description)
|
||||||
* Include screenshot (if needed)
|
* Include screenshot (if needed)
|
||||||
* If it could be device-dependent, try reproducing on another device (if possible)
|
* If it could be device-dependent, try reproducing on another device (if possible)
|
||||||
|
|||||||
1
app/.gitignore
vendored
1
app/.gitignore
vendored
@@ -1,4 +1,3 @@
|
|||||||
/build
|
/build
|
||||||
*iml
|
*iml
|
||||||
*.iml
|
*.iml
|
||||||
custom.gradle
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||||
import org.jmailen.gradle.kotlinter.tasks.LintTask
|
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("com.android.application")
|
id("com.android.application")
|
||||||
@@ -22,8 +21,9 @@ android {
|
|||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "eu.kanade.tachiyomi"
|
applicationId = "eu.kanade.tachiyomi"
|
||||||
versionCode = 95
|
|
||||||
versionName = "0.14.4"
|
versionCode = 108
|
||||||
|
versionName = "0.14.7"
|
||||||
|
|
||||||
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
||||||
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
|
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
|
||||||
@@ -65,11 +65,11 @@ android {
|
|||||||
initWith(getByName("release"))
|
initWith(getByName("release"))
|
||||||
buildConfigField("boolean", "PREVIEW", "true")
|
buildConfigField("boolean", "PREVIEW", "true")
|
||||||
|
|
||||||
|
signingConfig = signingConfigs.getByName("debug")
|
||||||
|
matchingFallbacks.add("release")
|
||||||
val debugType = getByName("debug")
|
val debugType = getByName("debug")
|
||||||
signingConfig = debugType.signingConfig
|
|
||||||
versionNameSuffix = debugType.versionNameSuffix
|
versionNameSuffix = debugType.versionNameSuffix
|
||||||
applicationIdSuffix = debugType.applicationIdSuffix
|
applicationIdSuffix = debugType.applicationIdSuffix
|
||||||
matchingFallbacks.add("release")
|
|
||||||
}
|
}
|
||||||
create("benchmark") {
|
create("benchmark") {
|
||||||
initWith(getByName("release"))
|
initWith(getByName("release"))
|
||||||
@@ -77,6 +77,7 @@ android {
|
|||||||
signingConfig = signingConfigs.getByName("debug")
|
signingConfig = signingConfigs.getByName("debug")
|
||||||
matchingFallbacks.add("release")
|
matchingFallbacks.add("release")
|
||||||
isDebuggable = false
|
isDebuggable = false
|
||||||
|
isProfileable = true
|
||||||
versionNameSuffix = "-benchmark"
|
versionNameSuffix = "-benchmark"
|
||||||
applicationIdSuffix = ".benchmark"
|
applicationIdSuffix = ".benchmark"
|
||||||
}
|
}
|
||||||
@@ -101,16 +102,18 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
packagingOptions {
|
packaging {
|
||||||
resources.excludes.addAll(listOf(
|
resources.excludes.addAll(
|
||||||
"META-INF/DEPENDENCIES",
|
listOf(
|
||||||
"LICENSE.txt",
|
"META-INF/DEPENDENCIES",
|
||||||
"META-INF/LICENSE",
|
"LICENSE.txt",
|
||||||
"META-INF/LICENSE.txt",
|
"META-INF/LICENSE",
|
||||||
"META-INF/README.md",
|
"META-INF/LICENSE.txt",
|
||||||
"META-INF/NOTICE",
|
"META-INF/README.md",
|
||||||
"META-INF/*.kotlin_module",
|
"META-INF/NOTICE",
|
||||||
))
|
"META-INF/*.kotlin_module",
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
dependenciesInfo {
|
dependenciesInfo {
|
||||||
@@ -140,7 +143,9 @@ android {
|
|||||||
dependencies {
|
dependencies {
|
||||||
implementation(project(":i18n"))
|
implementation(project(":i18n"))
|
||||||
implementation(project(":core"))
|
implementation(project(":core"))
|
||||||
|
implementation(project(":core-metadata"))
|
||||||
implementation(project(":source-api"))
|
implementation(project(":source-api"))
|
||||||
|
implementation(project(":source-local"))
|
||||||
implementation(project(":data"))
|
implementation(project(":data"))
|
||||||
implementation(project(":domain"))
|
implementation(project(":domain"))
|
||||||
implementation(project(":presentation-core"))
|
implementation(project(":presentation-core"))
|
||||||
@@ -155,10 +160,10 @@ dependencies {
|
|||||||
implementation(compose.material.icons)
|
implementation(compose.material.icons)
|
||||||
implementation(compose.animation)
|
implementation(compose.animation)
|
||||||
implementation(compose.animation.graphics)
|
implementation(compose.animation.graphics)
|
||||||
implementation(compose.ui.tooling)
|
debugImplementation(compose.ui.tooling)
|
||||||
|
implementation(compose.ui.tooling.preview)
|
||||||
implementation(compose.ui.util)
|
implementation(compose.ui.util)
|
||||||
implementation(compose.accompanist.webview)
|
implementation(compose.accompanist.webview)
|
||||||
implementation(compose.accompanist.flowlayout)
|
|
||||||
implementation(compose.accompanist.permissions)
|
implementation(compose.accompanist.permissions)
|
||||||
implementation(compose.accompanist.themeadapter)
|
implementation(compose.accompanist.themeadapter)
|
||||||
implementation(compose.accompanist.systemuicontroller)
|
implementation(compose.accompanist.systemuicontroller)
|
||||||
@@ -178,7 +183,6 @@ dependencies {
|
|||||||
implementation(androidx.appcompat)
|
implementation(androidx.appcompat)
|
||||||
implementation(androidx.biometricktx)
|
implementation(androidx.biometricktx)
|
||||||
implementation(androidx.constraintlayout)
|
implementation(androidx.constraintlayout)
|
||||||
implementation(androidx.coordinatorlayout)
|
|
||||||
implementation(androidx.corektx)
|
implementation(androidx.corektx)
|
||||||
implementation(androidx.splashscreen)
|
implementation(androidx.splashscreen)
|
||||||
implementation(androidx.recyclerview)
|
implementation(androidx.recyclerview)
|
||||||
@@ -188,20 +192,18 @@ dependencies {
|
|||||||
implementation(androidx.bundles.lifecycle)
|
implementation(androidx.bundles.lifecycle)
|
||||||
|
|
||||||
// Job scheduling
|
// Job scheduling
|
||||||
implementation(androidx.bundles.workmanager)
|
implementation(androidx.workmanager)
|
||||||
|
|
||||||
// RX
|
// RxJava
|
||||||
implementation(libs.bundles.reactivex)
|
implementation(libs.rxjava)
|
||||||
implementation(libs.flowreactivenetwork)
|
implementation(libs.flowreactivenetwork)
|
||||||
|
|
||||||
// Network client
|
// Networking
|
||||||
implementation(libs.bundles.okhttp)
|
implementation(libs.bundles.okhttp)
|
||||||
implementation(libs.okio)
|
implementation(libs.okio)
|
||||||
|
implementation(libs.conscrypt.android) // TLS 1.3 support for Android < 10
|
||||||
|
|
||||||
// TLS 1.3 support for Android < 10
|
// Data serialization (JSON, protobuf, xml)
|
||||||
implementation(libs.conscrypt.android)
|
|
||||||
|
|
||||||
// Data serialization (JSON, protobuf)
|
|
||||||
implementation(kotlinx.bundles.serialization)
|
implementation(kotlinx.bundles.serialization)
|
||||||
|
|
||||||
// HTML parser
|
// HTML parser
|
||||||
@@ -219,19 +221,16 @@ dependencies {
|
|||||||
implementation(libs.injekt.core)
|
implementation(libs.injekt.core)
|
||||||
|
|
||||||
// Image loading
|
// Image loading
|
||||||
|
implementation(platform(libs.coil.bom))
|
||||||
implementation(libs.bundles.coil)
|
implementation(libs.bundles.coil)
|
||||||
implementation(libs.subsamplingscaleimageview) {
|
implementation(libs.subsamplingscaleimageview) {
|
||||||
exclude(module = "image-decoder")
|
exclude(module = "image-decoder")
|
||||||
}
|
}
|
||||||
implementation(libs.image.decoder)
|
implementation(libs.image.decoder)
|
||||||
|
|
||||||
// Sort
|
|
||||||
implementation(libs.natural.comparator)
|
|
||||||
|
|
||||||
// UI libraries
|
// UI libraries
|
||||||
implementation(libs.material)
|
implementation(libs.material)
|
||||||
implementation(libs.flexible.adapter.core)
|
implementation(libs.flexible.adapter.core)
|
||||||
implementation(libs.flexible.adapter.ui)
|
|
||||||
implementation(libs.photoview)
|
implementation(libs.photoview)
|
||||||
implementation(libs.directionalviewpager) {
|
implementation(libs.directionalviewpager) {
|
||||||
exclude(group = "androidx.viewpager", module = "viewpager")
|
exclude(group = "androidx.viewpager", module = "viewpager")
|
||||||
@@ -239,10 +238,9 @@ dependencies {
|
|||||||
implementation(libs.insetter)
|
implementation(libs.insetter)
|
||||||
implementation(libs.bundles.richtext)
|
implementation(libs.bundles.richtext)
|
||||||
implementation(libs.aboutLibraries.compose)
|
implementation(libs.aboutLibraries.compose)
|
||||||
implementation(libs.cascade)
|
|
||||||
implementation(libs.bundles.voyager)
|
implementation(libs.bundles.voyager)
|
||||||
implementation(libs.wheelpicker)
|
implementation(libs.compose.materialmotion)
|
||||||
implementation(libs.materialmotion.core)
|
implementation(libs.swipe)
|
||||||
|
|
||||||
// Logging
|
// Logging
|
||||||
implementation(libs.logcat)
|
implementation(libs.logcat)
|
||||||
@@ -255,7 +253,7 @@ dependencies {
|
|||||||
implementation(libs.bundles.shizuku)
|
implementation(libs.bundles.shizuku)
|
||||||
|
|
||||||
// Tests
|
// Tests
|
||||||
testImplementation(libs.junit)
|
testImplementation(libs.bundles.test)
|
||||||
|
|
||||||
// For detecting memory leaks; see https://square.github.io/leakcanary/
|
// For detecting memory leaks; see https://square.github.io/leakcanary/
|
||||||
// debugImplementation(libs.leakcanary.android)
|
// debugImplementation(libs.leakcanary.android)
|
||||||
@@ -266,7 +264,9 @@ androidComponents {
|
|||||||
beforeVariants { variantBuilder ->
|
beforeVariants { variantBuilder ->
|
||||||
// Disables standardBenchmark
|
// Disables standardBenchmark
|
||||||
if (variantBuilder.buildType == "benchmark") {
|
if (variantBuilder.buildType == "benchmark") {
|
||||||
variantBuilder.enable = variantBuilder.productFlavors.containsAll(listOf("default" to "dev"))
|
variantBuilder.enable = variantBuilder.productFlavors.containsAll(
|
||||||
|
listOf("default" to "dev"),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
onVariants(selector().withFlavor("default" to "standard")) {
|
onVariants(selector().withFlavor("default" to "standard")) {
|
||||||
@@ -277,16 +277,10 @@ androidComponents {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tasks {
|
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)
|
// See https://kotlinlang.org/docs/reference/experimental.html#experimental-status-of-experimental-api(-markers)
|
||||||
withType<KotlinCompile> {
|
withType<KotlinCompile> {
|
||||||
kotlinOptions.freeCompilerArgs += listOf(
|
kotlinOptions.freeCompilerArgs += listOf(
|
||||||
"-opt-in=coil.annotation.ExperimentalCoilApi",
|
"-Xcontext-receivers",
|
||||||
"-opt-in=com.google.accompanist.permissions.ExperimentalPermissionsApi",
|
|
||||||
"-opt-in=androidx.compose.foundation.layout.ExperimentalLayoutApi",
|
"-opt-in=androidx.compose.foundation.layout.ExperimentalLayoutApi",
|
||||||
"-opt-in=androidx.compose.material.ExperimentalMaterialApi",
|
"-opt-in=androidx.compose.material.ExperimentalMaterialApi",
|
||||||
"-opt-in=androidx.compose.material3.ExperimentalMaterial3Api",
|
"-opt-in=androidx.compose.material3.ExperimentalMaterial3Api",
|
||||||
@@ -295,6 +289,8 @@ tasks {
|
|||||||
"-opt-in=androidx.compose.foundation.ExperimentalFoundationApi",
|
"-opt-in=androidx.compose.foundation.ExperimentalFoundationApi",
|
||||||
"-opt-in=androidx.compose.animation.ExperimentalAnimationApi",
|
"-opt-in=androidx.compose.animation.ExperimentalAnimationApi",
|
||||||
"-opt-in=androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi",
|
"-opt-in=androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi",
|
||||||
|
"-opt-in=coil.annotation.ExperimentalCoilApi",
|
||||||
|
"-opt-in=com.google.accompanist.permissions.ExperimentalPermissionsApi",
|
||||||
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
||||||
"-opt-in=kotlinx.coroutines.FlowPreview",
|
"-opt-in=kotlinx.coroutines.FlowPreview",
|
||||||
"-opt-in=kotlinx.coroutines.InternalCoroutinesApi",
|
"-opt-in=kotlinx.coroutines.InternalCoroutinesApi",
|
||||||
@@ -305,12 +301,12 @@ tasks {
|
|||||||
kotlinOptions.freeCompilerArgs += listOf(
|
kotlinOptions.freeCompilerArgs += listOf(
|
||||||
"-P",
|
"-P",
|
||||||
"plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=" +
|
"plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=" +
|
||||||
project.buildDir.absolutePath + "/compose_metrics"
|
project.layout.buildDirectory.dir("compose_metrics").get().asFile.absolutePath,
|
||||||
)
|
)
|
||||||
kotlinOptions.freeCompilerArgs += listOf(
|
kotlinOptions.freeCompilerArgs += listOf(
|
||||||
"-P",
|
"-P",
|
||||||
"plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=" +
|
"plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=" +
|
||||||
project.buildDir.absolutePath + "/compose_metrics"
|
project.layout.buildDirectory.dir("compose_metrics").get().asFile.absolutePath,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
-dontusemixedcaseclassnames
|
-dontusemixedcaseclassnames
|
||||||
|
-ignorewarnings
|
||||||
-verbose
|
-verbose
|
||||||
|
|
||||||
-keepattributes *Annotation*
|
-keepattributes *Annotation*
|
||||||
@@ -13,7 +14,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
-keepclassmembers class * implements android.os.Parcelable {
|
-keepclassmembers class * implements android.os.Parcelable {
|
||||||
public static final ** CREATOR;
|
public static final ** CREATOR;
|
||||||
}
|
}
|
||||||
|
|
||||||
-keep class androidx.annotation.Keep
|
-keep class androidx.annotation.Keep
|
||||||
|
|||||||
12
app/proguard-rules.pro
vendored
12
app/proguard-rules.pro
vendored
@@ -1,14 +1,18 @@
|
|||||||
-dontobfuscate
|
-dontobfuscate
|
||||||
|
|
||||||
|
-keep,allowoptimization class eu.kanade.**
|
||||||
|
-keep,allowoptimization class tachiyomi.**
|
||||||
|
|
||||||
# Keep common dependencies used in extensions
|
# Keep common dependencies used in extensions
|
||||||
-keep,allowoptimization class androidx.preference.** { public protected *; }
|
-keep,allowoptimization class androidx.preference.** { public protected *; }
|
||||||
-keep,allowoptimization class kotlin.** { public protected *; }
|
-keep,allowoptimization class kotlin.** { public protected *; }
|
||||||
-keep,allowoptimization class kotlinx.coroutines.** { public protected *; }
|
-keep,allowoptimization class kotlinx.coroutines.** { public protected *; }
|
||||||
-keep,allowoptimization class kotlinx.serialization.** { public protected *; }
|
-keep,allowoptimization class kotlinx.serialization.** { public protected *; }
|
||||||
|
-keep,allowoptimization class kotlin.time.** { public protected *; }
|
||||||
-keep,allowoptimization class okhttp3.** { public protected *; }
|
-keep,allowoptimization class okhttp3.** { public protected *; }
|
||||||
-keep,allowoptimization class okio.** { public protected *; }
|
-keep,allowoptimization class okio.** { public protected *; }
|
||||||
-keep,allowoptimization class rx.** { public protected *; }
|
|
||||||
-keep,allowoptimization class org.jsoup.** { public protected *; }
|
-keep,allowoptimization class org.jsoup.** { public protected *; }
|
||||||
|
-keep,allowoptimization class rx.** { public protected *; }
|
||||||
-keep,allowoptimization class app.cash.quickjs.** { public protected *; }
|
-keep,allowoptimization class app.cash.quickjs.** { public protected *; }
|
||||||
-keep,allowoptimization class uy.kohesive.injekt.** { public protected *; }
|
-keep,allowoptimization class uy.kohesive.injekt.** { public protected *; }
|
||||||
|
|
||||||
@@ -66,4 +70,8 @@
|
|||||||
##---------------End: proguard configuration for kotlinx.serialization ----------
|
##---------------End: proguard configuration for kotlinx.serialization ----------
|
||||||
|
|
||||||
# XmlUtil
|
# XmlUtil
|
||||||
-keep public enum nl.adaptivity.xmlutil.EventType { *; }
|
-keep public enum nl.adaptivity.xmlutil.EventType { *; }
|
||||||
|
|
||||||
|
# Firebase
|
||||||
|
-keep class com.google.firebase.installations.** { *; }
|
||||||
|
-keep interface com.google.firebase.installations.** { *; }
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
|
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
<shortcut
|
<shortcut
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:icon="@drawable/sc_collections_bookmark_48dp"
|
android:icon="@drawable/sc_collections_bookmark_48dp"
|
||||||
|
|||||||
@@ -32,21 +32,17 @@
|
|||||||
<application
|
<application
|
||||||
android:name=".App"
|
android:name=".App"
|
||||||
android:allowBackup="false"
|
android:allowBackup="false"
|
||||||
|
android:enableOnBackInvokedCallback="true"
|
||||||
android:hardwareAccelerated="true"
|
android:hardwareAccelerated="true"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:largeHeap="true"
|
android:largeHeap="true"
|
||||||
android:localeConfig="@xml/locales_config"
|
android:localeConfig="@xml/locales_config"
|
||||||
|
android:networkSecurityConfig="@xml/network_security_config"
|
||||||
android:requestLegacyExternalStorage="true"
|
android:requestLegacyExternalStorage="true"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:theme="@style/Theme.Tachiyomi"
|
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:networkSecurityConfig="@xml/network_security_config">
|
android:theme="@style/Theme.Tachiyomi">
|
||||||
|
|
||||||
<!-- enable profiling by macrobenchmark -->
|
|
||||||
<profileable
|
|
||||||
android:shell="true"
|
|
||||||
tools:targetApi="q" />
|
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.main.MainActivity"
|
android:name=".ui.main.MainActivity"
|
||||||
@@ -69,10 +65,10 @@
|
|||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.main.DeepLinkActivity"
|
android:name=".ui.deeplink.DeepLinkActivity"
|
||||||
android:launchMode="singleTask"
|
android:launchMode="singleTask"
|
||||||
android:theme="@android:style/Theme.NoDisplay"
|
android:theme="@android:style/Theme.NoDisplay"
|
||||||
android:label="@string/action_global_search"
|
android:label="@string/action_search"
|
||||||
android:exported="true">
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.SEARCH" />
|
<action android:name="android.intent.action.SEARCH" />
|
||||||
@@ -123,8 +119,8 @@
|
|||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.setting.track.AnilistLoginActivity"
|
android:name=".ui.setting.track.TrackLoginActivity"
|
||||||
android:label="Anilist"
|
android:label="@string/track_activity_name"
|
||||||
android:exported="true">
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
@@ -132,54 +128,12 @@
|
|||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
<data
|
<data android:host="anilist-auth"/>
|
||||||
android:host="anilist-auth"
|
<data android:host="bangumi-auth"/>
|
||||||
android:scheme="tachiyomi" />
|
<data android:host="myanimelist-auth"/>
|
||||||
</intent-filter>
|
<data android:host="shikimori-auth"/>
|
||||||
</activity>
|
|
||||||
<activity
|
|
||||||
android:name=".ui.setting.track.MyAnimeListLoginActivity"
|
|
||||||
android:label="MyAnimeList"
|
|
||||||
android:exported="true">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.VIEW" />
|
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<data android:scheme="tachiyomi"/>
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
|
||||||
|
|
||||||
<data
|
|
||||||
android:host="myanimelist-auth"
|
|
||||||
android:scheme="tachiyomi" />
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
|
||||||
<activity
|
|
||||||
android:name=".ui.setting.track.ShikimoriLoginActivity"
|
|
||||||
android:label="Shikimori"
|
|
||||||
android:exported="true">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.VIEW" />
|
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
|
||||||
|
|
||||||
<data
|
|
||||||
android:host="shikimori-auth"
|
|
||||||
android:scheme="tachiyomi" />
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
|
||||||
<activity
|
|
||||||
android:name=".ui.setting.track.BangumiLoginActivity"
|
|
||||||
android:label="Bangumi"
|
|
||||||
android:exported="true">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.VIEW" />
|
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
|
||||||
|
|
||||||
<data
|
|
||||||
android:host="bangumi-auth"
|
|
||||||
android:scheme="tachiyomi" />
|
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
@@ -187,37 +141,12 @@
|
|||||||
android:name=".data.notification.NotificationReceiver"
|
android:name=".data.notification.NotificationReceiver"
|
||||||
android:exported="false" />
|
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" />
|
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".data.download.DownloadService"
|
android:name=".data.download.DownloadService"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".data.updater.AppUpdateService"
|
android:name=".extension.util.ExtensionInstallService"
|
||||||
android:exported="false" />
|
|
||||||
|
|
||||||
<service
|
|
||||||
android:name=".data.backup.BackupRestoreService"
|
|
||||||
android:exported="false" />
|
|
||||||
|
|
||||||
<service android:name=".extension.util.ExtensionInstallService"
|
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
<service
|
<service
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
10
app/src/main/java/eu/kanade/core/preference/CheckboxState.kt
Normal file
10
app/src/main/java/eu/kanade/core/preference/CheckboxState.kt
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
package eu.kanade.core.preference
|
||||||
|
|
||||||
|
import androidx.compose.ui.state.ToggleableState
|
||||||
|
import tachiyomi.core.preference.CheckboxState
|
||||||
|
|
||||||
|
fun <T> CheckboxState.TriState<T>.asToggleableState() = when (this) {
|
||||||
|
is CheckboxState.TriState.Exclude -> ToggleableState.Indeterminate
|
||||||
|
is CheckboxState.TriState.Include -> ToggleableState.On
|
||||||
|
is CheckboxState.TriState.None -> ToggleableState.Off
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package eu.kanade.core.prefs
|
package eu.kanade.core.preference
|
||||||
|
|
||||||
import androidx.compose.runtime.MutableState
|
import androidx.compose.runtime.MutableState
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
@@ -31,7 +31,7 @@ class PreferenceMutableState<T>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun component2(): (T) -> Unit {
|
override fun component2(): (T) -> Unit {
|
||||||
return { preference.set(it) }
|
return preference::set
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
package eu.kanade.core.util
|
package eu.kanade.core.util
|
||||||
|
|
||||||
import androidx.compose.ui.util.fastForEach
|
import androidx.compose.ui.util.fastForEach
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
|
||||||
import kotlin.contracts.ExperimentalContracts
|
import kotlin.contracts.ExperimentalContracts
|
||||||
import kotlin.contracts.contract
|
import kotlin.contracts.contract
|
||||||
|
|
||||||
@@ -12,23 +11,14 @@ fun <T : R, R : Any> List<T>.insertSeparators(
|
|||||||
val newList = mutableListOf<R>()
|
val newList = mutableListOf<R>()
|
||||||
for (i in -1..lastIndex) {
|
for (i in -1..lastIndex) {
|
||||||
val before = getOrNull(i)
|
val before = getOrNull(i)
|
||||||
before?.let { newList.add(it) }
|
before?.let(newList::add)
|
||||||
val after = getOrNull(i + 1)
|
val after = getOrNull(i + 1)
|
||||||
val separator = generator.invoke(before, after)
|
val separator = generator.invoke(before, after)
|
||||||
separator?.let { newList.add(it) }
|
separator?.let(newList::add)
|
||||||
}
|
}
|
||||||
return newList
|
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) {
|
fun <E> HashSet<E>.addOrRemove(value: E, shouldAdd: Boolean) {
|
||||||
if (shouldAdd) {
|
if (shouldAdd) {
|
||||||
add(value)
|
add(value)
|
||||||
@@ -80,7 +70,7 @@ inline fun <T, R> List<T>.fastMapNotNull(transform: (T) -> R?): List<R> {
|
|||||||
contract { callsInPlace(transform) }
|
contract { callsInPlace(transform) }
|
||||||
val destination = ArrayList<R>()
|
val destination = ArrayList<R>()
|
||||||
fastForEach { element ->
|
fastForEach { element ->
|
||||||
transform(element)?.let { destination.add(it) }
|
transform(element)?.let(destination::add)
|
||||||
}
|
}
|
||||||
return destination
|
return destination
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
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 }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
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,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,69 +1,78 @@
|
|||||||
package eu.kanade.domain
|
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.SetReadStatus
|
||||||
import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource
|
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.download.interactor.DeleteDownload
|
||||||
import eu.kanade.domain.extension.interactor.GetExtensionLanguages
|
import eu.kanade.domain.extension.interactor.GetExtensionLanguages
|
||||||
import eu.kanade.domain.extension.interactor.GetExtensionSources
|
import eu.kanade.domain.extension.interactor.GetExtensionSources
|
||||||
import eu.kanade.domain.extension.interactor.GetExtensionsByType
|
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.SetMangaViewerFlags
|
||||||
import eu.kanade.domain.manga.interactor.UpdateManga
|
import eu.kanade.domain.manga.interactor.UpdateManga
|
||||||
import eu.kanade.domain.source.interactor.GetEnabledSources
|
import eu.kanade.domain.source.interactor.GetEnabledSources
|
||||||
import eu.kanade.domain.source.interactor.GetLanguagesWithSources
|
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.GetSourcesWithFavoriteCount
|
||||||
import eu.kanade.domain.source.interactor.GetSourcesWithNonLibraryManga
|
|
||||||
import eu.kanade.domain.source.interactor.SetMigrateSorting
|
import eu.kanade.domain.source.interactor.SetMigrateSorting
|
||||||
import eu.kanade.domain.source.interactor.ToggleLanguage
|
import eu.kanade.domain.source.interactor.ToggleLanguage
|
||||||
import eu.kanade.domain.source.interactor.ToggleSource
|
import eu.kanade.domain.source.interactor.ToggleSource
|
||||||
import eu.kanade.domain.source.interactor.ToggleSourcePin
|
import eu.kanade.domain.source.interactor.ToggleSourcePin
|
||||||
import eu.kanade.domain.source.repository.SourceRepository
|
import eu.kanade.domain.track.interactor.AddTracks
|
||||||
import eu.kanade.domain.track.interactor.DeleteTrack
|
import eu.kanade.domain.track.interactor.RefreshTracks
|
||||||
import eu.kanade.domain.track.interactor.GetTracks
|
import eu.kanade.domain.track.interactor.SyncChapterProgressWithTrack
|
||||||
import eu.kanade.domain.track.interactor.GetTracksPerManga
|
import eu.kanade.domain.track.interactor.TrackChapter
|
||||||
import eu.kanade.domain.track.interactor.InsertTrack
|
|
||||||
import tachiyomi.data.category.CategoryRepositoryImpl
|
import tachiyomi.data.category.CategoryRepositoryImpl
|
||||||
import tachiyomi.data.chapter.ChapterRepositoryImpl
|
import tachiyomi.data.chapter.ChapterRepositoryImpl
|
||||||
import tachiyomi.data.history.HistoryRepositoryImpl
|
import tachiyomi.data.history.HistoryRepositoryImpl
|
||||||
import tachiyomi.data.manga.MangaRepositoryImpl
|
import tachiyomi.data.manga.MangaRepositoryImpl
|
||||||
import tachiyomi.data.source.SourceDataRepositoryImpl
|
import tachiyomi.data.release.ReleaseServiceImpl
|
||||||
|
import tachiyomi.data.source.SourceRepositoryImpl
|
||||||
|
import tachiyomi.data.source.StubSourceRepositoryImpl
|
||||||
import tachiyomi.data.track.TrackRepositoryImpl
|
import tachiyomi.data.track.TrackRepositoryImpl
|
||||||
import tachiyomi.data.updates.UpdatesRepositoryImpl
|
import tachiyomi.data.updates.UpdatesRepositoryImpl
|
||||||
|
import tachiyomi.domain.category.interactor.CreateCategoryWithName
|
||||||
|
import tachiyomi.domain.category.interactor.DeleteCategory
|
||||||
import tachiyomi.domain.category.interactor.GetCategories
|
import tachiyomi.domain.category.interactor.GetCategories
|
||||||
|
import tachiyomi.domain.category.interactor.RenameCategory
|
||||||
|
import tachiyomi.domain.category.interactor.ReorderCategory
|
||||||
|
import tachiyomi.domain.category.interactor.ResetCategoryFlags
|
||||||
|
import tachiyomi.domain.category.interactor.SetDisplayMode
|
||||||
|
import tachiyomi.domain.category.interactor.SetMangaCategories
|
||||||
|
import tachiyomi.domain.category.interactor.SetSortModeForCategory
|
||||||
|
import tachiyomi.domain.category.interactor.UpdateCategory
|
||||||
import tachiyomi.domain.category.repository.CategoryRepository
|
import tachiyomi.domain.category.repository.CategoryRepository
|
||||||
|
import tachiyomi.domain.chapter.interactor.GetChapter
|
||||||
|
import tachiyomi.domain.chapter.interactor.GetChapterByUrlAndMangaId
|
||||||
|
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
|
||||||
|
import tachiyomi.domain.chapter.interactor.SetMangaDefaultChapterFlags
|
||||||
import tachiyomi.domain.chapter.interactor.ShouldUpdateDbChapter
|
import tachiyomi.domain.chapter.interactor.ShouldUpdateDbChapter
|
||||||
|
import tachiyomi.domain.chapter.interactor.UpdateChapter
|
||||||
import tachiyomi.domain.chapter.repository.ChapterRepository
|
import tachiyomi.domain.chapter.repository.ChapterRepository
|
||||||
import tachiyomi.domain.history.interactor.GetHistory
|
import tachiyomi.domain.history.interactor.GetHistory
|
||||||
|
import tachiyomi.domain.history.interactor.GetNextChapters
|
||||||
import tachiyomi.domain.history.interactor.GetTotalReadDuration
|
import tachiyomi.domain.history.interactor.GetTotalReadDuration
|
||||||
import tachiyomi.domain.history.interactor.RemoveHistory
|
import tachiyomi.domain.history.interactor.RemoveHistory
|
||||||
import tachiyomi.domain.history.interactor.UpsertHistory
|
import tachiyomi.domain.history.interactor.UpsertHistory
|
||||||
import tachiyomi.domain.history.repository.HistoryRepository
|
import tachiyomi.domain.history.repository.HistoryRepository
|
||||||
|
import tachiyomi.domain.manga.interactor.FetchInterval
|
||||||
|
import tachiyomi.domain.manga.interactor.GetDuplicateLibraryManga
|
||||||
|
import tachiyomi.domain.manga.interactor.GetFavorites
|
||||||
|
import tachiyomi.domain.manga.interactor.GetLibraryManga
|
||||||
|
import tachiyomi.domain.manga.interactor.GetManga
|
||||||
|
import tachiyomi.domain.manga.interactor.GetMangaByUrlAndSourceId
|
||||||
|
import tachiyomi.domain.manga.interactor.GetMangaWithChapters
|
||||||
|
import tachiyomi.domain.manga.interactor.NetworkToLocalManga
|
||||||
|
import tachiyomi.domain.manga.interactor.ResetViewerFlags
|
||||||
|
import tachiyomi.domain.manga.interactor.SetMangaChapterFlags
|
||||||
import tachiyomi.domain.manga.repository.MangaRepository
|
import tachiyomi.domain.manga.repository.MangaRepository
|
||||||
import tachiyomi.domain.source.repository.SourceDataRepository
|
import tachiyomi.domain.release.interactor.GetApplicationRelease
|
||||||
|
import tachiyomi.domain.release.service.ReleaseService
|
||||||
|
import tachiyomi.domain.source.interactor.GetRemoteManga
|
||||||
|
import tachiyomi.domain.source.interactor.GetSourcesWithNonLibraryManga
|
||||||
|
import tachiyomi.domain.source.repository.SourceRepository
|
||||||
|
import tachiyomi.domain.source.repository.StubSourceRepository
|
||||||
|
import tachiyomi.domain.track.interactor.DeleteTrack
|
||||||
|
import tachiyomi.domain.track.interactor.GetTracks
|
||||||
|
import tachiyomi.domain.track.interactor.GetTracksPerManga
|
||||||
|
import tachiyomi.domain.track.interactor.InsertTrack
|
||||||
import tachiyomi.domain.track.repository.TrackRepository
|
import tachiyomi.domain.track.repository.TrackRepository
|
||||||
import tachiyomi.domain.updates.interactor.GetUpdates
|
import tachiyomi.domain.updates.interactor.GetUpdates
|
||||||
import tachiyomi.domain.updates.repository.UpdatesRepository
|
import tachiyomi.domain.updates.repository.UpdatesRepository
|
||||||
@@ -79,7 +88,7 @@ class DomainModule : InjektModule {
|
|||||||
addSingletonFactory<CategoryRepository> { CategoryRepositoryImpl(get()) }
|
addSingletonFactory<CategoryRepository> { CategoryRepositoryImpl(get()) }
|
||||||
addFactory { GetCategories(get()) }
|
addFactory { GetCategories(get()) }
|
||||||
addFactory { ResetCategoryFlags(get(), get()) }
|
addFactory { ResetCategoryFlags(get(), get()) }
|
||||||
addFactory { SetDisplayModeForCategory(get(), get()) }
|
addFactory { SetDisplayMode(get()) }
|
||||||
addFactory { SetSortModeForCategory(get(), get()) }
|
addFactory { SetSortModeForCategory(get(), get()) }
|
||||||
addFactory { CreateCategoryWithName(get(), get()) }
|
addFactory { CreateCategoryWithName(get(), get()) }
|
||||||
addFactory { RenameCategory(get()) }
|
addFactory { RenameCategory(get()) }
|
||||||
@@ -92,30 +101,39 @@ class DomainModule : InjektModule {
|
|||||||
addFactory { GetFavorites(get()) }
|
addFactory { GetFavorites(get()) }
|
||||||
addFactory { GetLibraryManga(get()) }
|
addFactory { GetLibraryManga(get()) }
|
||||||
addFactory { GetMangaWithChapters(get(), get()) }
|
addFactory { GetMangaWithChapters(get(), get()) }
|
||||||
|
addFactory { GetMangaByUrlAndSourceId(get()) }
|
||||||
addFactory { GetManga(get()) }
|
addFactory { GetManga(get()) }
|
||||||
addFactory { GetNextChapters(get(), get(), get()) }
|
addFactory { GetNextChapters(get(), get(), get()) }
|
||||||
addFactory { ResetViewerFlags(get()) }
|
addFactory { ResetViewerFlags(get()) }
|
||||||
addFactory { SetMangaChapterFlags(get()) }
|
addFactory { SetMangaChapterFlags(get()) }
|
||||||
|
addFactory { FetchInterval(get()) }
|
||||||
addFactory { SetMangaDefaultChapterFlags(get(), get(), get()) }
|
addFactory { SetMangaDefaultChapterFlags(get(), get(), get()) }
|
||||||
addFactory { SetMangaViewerFlags(get()) }
|
addFactory { SetMangaViewerFlags(get()) }
|
||||||
addFactory { NetworkToLocalManga(get()) }
|
addFactory { NetworkToLocalManga(get()) }
|
||||||
addFactory { UpdateManga(get()) }
|
addFactory { UpdateManga(get(), get()) }
|
||||||
addFactory { SetMangaCategories(get()) }
|
addFactory { SetMangaCategories(get()) }
|
||||||
|
|
||||||
|
addSingletonFactory<ReleaseService> { ReleaseServiceImpl(get(), get()) }
|
||||||
|
addFactory { GetApplicationRelease(get(), get()) }
|
||||||
|
|
||||||
addSingletonFactory<TrackRepository> { TrackRepositoryImpl(get()) }
|
addSingletonFactory<TrackRepository> { TrackRepositoryImpl(get()) }
|
||||||
|
addFactory { TrackChapter(get(), get(), get(), get()) }
|
||||||
|
addFactory { AddTracks(get(), get(), get(), get()) }
|
||||||
|
addFactory { RefreshTracks(get(), get(), get(), get()) }
|
||||||
addFactory { DeleteTrack(get()) }
|
addFactory { DeleteTrack(get()) }
|
||||||
addFactory { GetTracksPerManga(get()) }
|
addFactory { GetTracksPerManga(get()) }
|
||||||
addFactory { GetTracks(get()) }
|
addFactory { GetTracks(get()) }
|
||||||
addFactory { InsertTrack(get()) }
|
addFactory { InsertTrack(get()) }
|
||||||
|
addFactory { SyncChapterProgressWithTrack(get(), get(), get()) }
|
||||||
|
|
||||||
addSingletonFactory<ChapterRepository> { ChapterRepositoryImpl(get()) }
|
addSingletonFactory<ChapterRepository> { ChapterRepositoryImpl(get()) }
|
||||||
addFactory { GetChapter(get()) }
|
addFactory { GetChapter(get()) }
|
||||||
addFactory { GetChapterByMangaId(get()) }
|
addFactory { GetChaptersByMangaId(get()) }
|
||||||
|
addFactory { GetChapterByUrlAndMangaId(get()) }
|
||||||
addFactory { UpdateChapter(get()) }
|
addFactory { UpdateChapter(get()) }
|
||||||
addFactory { SetReadStatus(get(), get(), get(), get()) }
|
addFactory { SetReadStatus(get(), get(), get(), get()) }
|
||||||
addFactory { ShouldUpdateDbChapter() }
|
addFactory { ShouldUpdateDbChapter() }
|
||||||
addFactory { SyncChaptersWithSource(get(), get(), get(), get()) }
|
addFactory { SyncChaptersWithSource(get(), get(), get(), get(), get(), get(), get()) }
|
||||||
addFactory { SyncChaptersWithTrackServiceTwoWay(get(), get()) }
|
|
||||||
|
|
||||||
addSingletonFactory<HistoryRepository> { HistoryRepositoryImpl(get()) }
|
addSingletonFactory<HistoryRepository> { HistoryRepositoryImpl(get()) }
|
||||||
addFactory { GetHistory(get()) }
|
addFactory { GetHistory(get()) }
|
||||||
@@ -133,7 +151,7 @@ class DomainModule : InjektModule {
|
|||||||
addFactory { GetUpdates(get()) }
|
addFactory { GetUpdates(get()) }
|
||||||
|
|
||||||
addSingletonFactory<SourceRepository> { SourceRepositoryImpl(get(), get()) }
|
addSingletonFactory<SourceRepository> { SourceRepositoryImpl(get(), get()) }
|
||||||
addSingletonFactory<SourceDataRepository> { SourceDataRepositoryImpl(get()) }
|
addSingletonFactory<StubSourceRepository> { StubSourceRepositoryImpl(get()) }
|
||||||
addFactory { GetEnabledSources(get(), get()) }
|
addFactory { GetEnabledSources(get(), get()) }
|
||||||
addFactory { GetLanguagesWithSources(get(), get()) }
|
addFactory { GetLanguagesWithSources(get(), get()) }
|
||||||
addFactory { GetRemoteManga(get()) }
|
addFactory { GetRemoteManga(get()) }
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
package eu.kanade.domain.base
|
package eu.kanade.domain.base
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.util.system.isPreviewBuildType
|
import eu.kanade.tachiyomi.util.system.isPreviewBuildType
|
||||||
import eu.kanade.tachiyomi.util.system.isReleaseBuildType
|
import eu.kanade.tachiyomi.util.system.isReleaseBuildType
|
||||||
|
import tachiyomi.core.preference.Preference
|
||||||
import tachiyomi.core.preference.PreferenceStore
|
import tachiyomi.core.preference.PreferenceStore
|
||||||
|
|
||||||
class BasePreferences(
|
class BasePreferences(
|
||||||
@@ -10,15 +13,21 @@ class BasePreferences(
|
|||||||
private val preferenceStore: PreferenceStore,
|
private val preferenceStore: PreferenceStore,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun confirmExit() = preferenceStore.getBoolean("pref_confirm_exit", false)
|
fun downloadedOnly() = preferenceStore.getBoolean(
|
||||||
|
Preference.appStateKey("pref_downloaded_only"),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
|
||||||
fun downloadedOnly() = preferenceStore.getBoolean("pref_downloaded_only", false)
|
fun incognitoMode() = preferenceStore.getBoolean(Preference.appStateKey("incognito_mode"), false)
|
||||||
|
|
||||||
fun incognitoMode() = preferenceStore.getBoolean("incognito_mode", false)
|
|
||||||
|
|
||||||
fun automaticExtUpdates() = preferenceStore.getBoolean("automatic_ext_updates", true)
|
|
||||||
|
|
||||||
fun extensionInstaller() = ExtensionInstallerPreference(context, preferenceStore)
|
fun extensionInstaller() = ExtensionInstallerPreference(context, preferenceStore)
|
||||||
|
|
||||||
fun acraEnabled() = preferenceStore.getBoolean("acra.enable", isPreviewBuildType || isReleaseBuildType)
|
fun acraEnabled() = preferenceStore.getBoolean("acra.enable", isPreviewBuildType || isReleaseBuildType)
|
||||||
|
|
||||||
|
enum class ExtensionInstaller(@StringRes val titleResId: Int) {
|
||||||
|
LEGACY(R.string.ext_installer_legacy),
|
||||||
|
PACKAGEINSTALLER(R.string.ext_installer_packageinstaller),
|
||||||
|
SHIZUKU(R.string.ext_installer_shizuku),
|
||||||
|
PRIVATE(R.string.ext_installer_private),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package eu.kanade.domain.base
|
package eu.kanade.domain.base
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferenceValues.ExtensionInstaller
|
import eu.kanade.domain.base.BasePreferences.ExtensionInstaller
|
||||||
import eu.kanade.tachiyomi.util.system.hasMiuiPackageInstaller
|
import eu.kanade.tachiyomi.util.system.hasMiuiPackageInstaller
|
||||||
import eu.kanade.tachiyomi.util.system.isShizukuInstalled
|
import eu.kanade.tachiyomi.util.system.isShizukuInstalled
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
@@ -18,7 +18,7 @@ class ExtensionInstallerPreference(
|
|||||||
|
|
||||||
override fun key() = "extension_installer"
|
override fun key() = "extension_installer"
|
||||||
|
|
||||||
val entries get() = ExtensionInstaller.values().run {
|
val entries get() = ExtensionInstaller.entries.run {
|
||||||
if (context.hasMiuiPackageInstaller) {
|
if (context.hasMiuiPackageInstaller) {
|
||||||
filter { it != ExtensionInstaller.PACKAGEINSTALLER }
|
filter { it != ExtensionInstaller.PACKAGEINSTALLER }
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
package eu.kanade.domain.chapter.interactor
|
package eu.kanade.domain.chapter.interactor
|
||||||
|
|
||||||
import eu.kanade.domain.download.interactor.DeleteDownload
|
import eu.kanade.domain.download.interactor.DeleteDownload
|
||||||
import eu.kanade.domain.download.service.DownloadPreferences
|
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import tachiyomi.core.util.lang.withNonCancellableContext
|
import tachiyomi.core.util.lang.withNonCancellableContext
|
||||||
import tachiyomi.core.util.system.logcat
|
import tachiyomi.core.util.system.logcat
|
||||||
import tachiyomi.domain.chapter.model.Chapter
|
import tachiyomi.domain.chapter.model.Chapter
|
||||||
import tachiyomi.domain.chapter.model.ChapterUpdate
|
import tachiyomi.domain.chapter.model.ChapterUpdate
|
||||||
import tachiyomi.domain.chapter.repository.ChapterRepository
|
import tachiyomi.domain.chapter.repository.ChapterRepository
|
||||||
|
import tachiyomi.domain.download.service.DownloadPreferences
|
||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
import tachiyomi.domain.manga.repository.MangaRepository
|
import tachiyomi.domain.manga.repository.MangaRepository
|
||||||
|
|
||||||
@@ -72,9 +72,9 @@ class SetReadStatus(
|
|||||||
suspend fun await(manga: Manga, read: Boolean) =
|
suspend fun await(manga: Manga, read: Boolean) =
|
||||||
await(manga.id, read)
|
await(manga.id, read)
|
||||||
|
|
||||||
sealed class Result {
|
sealed interface Result {
|
||||||
object Success : Result()
|
data object Success : Result
|
||||||
object NoChapters : Result()
|
data object NoChapters : Result
|
||||||
data class InternalError(val error: Throwable) : Result()
|
data class InternalError(val error: Throwable) : Result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,31 +7,32 @@ import eu.kanade.domain.manga.model.toSManga
|
|||||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||||
import eu.kanade.tachiyomi.data.download.DownloadProvider
|
import eu.kanade.tachiyomi.data.download.DownloadProvider
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.source.isLocal
|
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.util.chapter.ChapterRecognition
|
|
||||||
import tachiyomi.data.chapter.ChapterSanitizer
|
import tachiyomi.data.chapter.ChapterSanitizer
|
||||||
|
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
|
||||||
import tachiyomi.domain.chapter.interactor.ShouldUpdateDbChapter
|
import tachiyomi.domain.chapter.interactor.ShouldUpdateDbChapter
|
||||||
|
import tachiyomi.domain.chapter.interactor.UpdateChapter
|
||||||
import tachiyomi.domain.chapter.model.Chapter
|
import tachiyomi.domain.chapter.model.Chapter
|
||||||
import tachiyomi.domain.chapter.model.NoChaptersException
|
import tachiyomi.domain.chapter.model.NoChaptersException
|
||||||
import tachiyomi.domain.chapter.model.toChapterUpdate
|
import tachiyomi.domain.chapter.model.toChapterUpdate
|
||||||
import tachiyomi.domain.chapter.repository.ChapterRepository
|
import tachiyomi.domain.chapter.repository.ChapterRepository
|
||||||
|
import tachiyomi.domain.chapter.service.ChapterRecognition
|
||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
import uy.kohesive.injekt.Injekt
|
import tachiyomi.source.local.isLocal
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
import java.lang.Long.max
|
import java.lang.Long.max
|
||||||
|
import java.time.ZonedDateTime
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.TreeSet
|
import java.util.TreeSet
|
||||||
|
|
||||||
class SyncChaptersWithSource(
|
class SyncChaptersWithSource(
|
||||||
private val downloadManager: DownloadManager = Injekt.get(),
|
private val downloadManager: DownloadManager,
|
||||||
private val downloadProvider: DownloadProvider = Injekt.get(),
|
private val downloadProvider: DownloadProvider,
|
||||||
private val chapterRepository: ChapterRepository = Injekt.get(),
|
private val chapterRepository: ChapterRepository,
|
||||||
private val shouldUpdateDbChapter: ShouldUpdateDbChapter = Injekt.get(),
|
private val shouldUpdateDbChapter: ShouldUpdateDbChapter,
|
||||||
private val updateManga: UpdateManga = Injekt.get(),
|
private val updateManga: UpdateManga,
|
||||||
private val updateChapter: UpdateChapter = Injekt.get(),
|
private val updateChapter: UpdateChapter,
|
||||||
private val getChapterByMangaId: GetChapterByMangaId = Injekt.get(),
|
private val getChaptersByMangaId: GetChaptersByMangaId,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -46,11 +47,15 @@ class SyncChaptersWithSource(
|
|||||||
rawSourceChapters: List<SChapter>,
|
rawSourceChapters: List<SChapter>,
|
||||||
manga: Manga,
|
manga: Manga,
|
||||||
source: Source,
|
source: Source,
|
||||||
|
manualFetch: Boolean = false,
|
||||||
|
fetchWindow: Pair<Long, Long> = Pair(0, 0),
|
||||||
): List<Chapter> {
|
): List<Chapter> {
|
||||||
if (rawSourceChapters.isEmpty() && !source.isLocal()) {
|
if (rawSourceChapters.isEmpty() && !source.isLocal()) {
|
||||||
throw NoChaptersException()
|
throw NoChaptersException()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val now = ZonedDateTime.now()
|
||||||
|
|
||||||
val sourceChapters = rawSourceChapters
|
val sourceChapters = rawSourceChapters
|
||||||
.distinctBy { it.url }
|
.distinctBy { it.url }
|
||||||
.mapIndexed { i, sChapter ->
|
.mapIndexed { i, sChapter ->
|
||||||
@@ -61,7 +66,7 @@ class SyncChaptersWithSource(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Chapters from db.
|
// Chapters from db.
|
||||||
val dbChapters = getChapterByMangaId.await(manga.id)
|
val dbChapters = getChaptersByMangaId.await(manga.id)
|
||||||
|
|
||||||
// Chapters from the source not in db.
|
// Chapters from the source not in db.
|
||||||
val toAdd = mutableListOf<Chapter>()
|
val toAdd = mutableListOf<Chapter>()
|
||||||
@@ -132,14 +137,21 @@ class SyncChaptersWithSource(
|
|||||||
|
|
||||||
// Return if there's nothing to add, delete or change, avoiding unnecessary db transactions.
|
// Return if there's nothing to add, delete or change, avoiding unnecessary db transactions.
|
||||||
if (toAdd.isEmpty() && toDelete.isEmpty() && toChange.isEmpty()) {
|
if (toAdd.isEmpty() && toDelete.isEmpty() && toChange.isEmpty()) {
|
||||||
|
if (manualFetch || manga.fetchInterval == 0 || manga.nextUpdate < fetchWindow.first) {
|
||||||
|
updateManga.awaitUpdateFetchInterval(
|
||||||
|
manga,
|
||||||
|
now,
|
||||||
|
fetchWindow,
|
||||||
|
)
|
||||||
|
}
|
||||||
return emptyList()
|
return emptyList()
|
||||||
}
|
}
|
||||||
|
|
||||||
val reAdded = mutableListOf<Chapter>()
|
val reAdded = mutableListOf<Chapter>()
|
||||||
|
|
||||||
val deletedChapterNumbers = TreeSet<Float>()
|
val deletedChapterNumbers = TreeSet<Double>()
|
||||||
val deletedReadChapterNumbers = TreeSet<Float>()
|
val deletedReadChapterNumbers = TreeSet<Double>()
|
||||||
val deletedBookmarkedChapterNumbers = TreeSet<Float>()
|
val deletedBookmarkedChapterNumbers = TreeSet<Double>()
|
||||||
|
|
||||||
toDelete.forEach { chapter ->
|
toDelete.forEach { chapter ->
|
||||||
if (chapter.read) deletedReadChapterNumbers.add(chapter.chapterNumber)
|
if (chapter.read) deletedReadChapterNumbers.add(chapter.chapterNumber)
|
||||||
@@ -186,6 +198,7 @@ class SyncChaptersWithSource(
|
|||||||
val chapterUpdates = toChange.map { it.toChapterUpdate() }
|
val chapterUpdates = toChange.map { it.toChapterUpdate() }
|
||||||
updateChapter.awaitAll(chapterUpdates)
|
updateChapter.awaitAll(chapterUpdates)
|
||||||
}
|
}
|
||||||
|
updateManga.awaitUpdateFetchInterval(manga, now, fetchWindow)
|
||||||
|
|
||||||
// Set this manga as updated since chapters were changed
|
// Set this manga as updated since chapters were changed
|
||||||
// Note that last_update actually represents last time the chapter list changed at all
|
// Note that last_update actually represents last time the chapter list changed at all
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ fun Chapter.toSChapter(): SChapter {
|
|||||||
it.url = url
|
it.url = url
|
||||||
it.name = name
|
it.name = name
|
||||||
it.date_upload = dateUpload
|
it.date_upload = dateUpload
|
||||||
it.chapter_number = chapterNumber
|
it.chapter_number = chapterNumber.toFloat()
|
||||||
it.scanlator = scanlator
|
it.scanlator = scanlator
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -21,7 +21,7 @@ fun Chapter.copyFromSChapter(sChapter: SChapter): Chapter {
|
|||||||
name = sChapter.name,
|
name = sChapter.name,
|
||||||
url = sChapter.url,
|
url = sChapter.url,
|
||||||
dateUpload = sChapter.date_upload,
|
dateUpload = sChapter.date_upload,
|
||||||
chapterNumber = sChapter.chapter_number,
|
chapterNumber = sChapter.chapter_number.toDouble(),
|
||||||
scanlator = sChapter.scanlator?.ifBlank { null },
|
scanlator = sChapter.scanlator?.ifBlank { null },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -37,6 +37,6 @@ fun Chapter.toDbChapter(): DbChapter = ChapterImpl().also {
|
|||||||
it.last_page_read = lastPageRead.toInt()
|
it.last_page_read = lastPageRead.toInt()
|
||||||
it.date_fetch = dateFetch
|
it.date_fetch = dateFetch
|
||||||
it.date_upload = dateUpload
|
it.date_upload = dateUpload
|
||||||
it.chapter_number = chapterNumber
|
it.chapter_number = chapterNumber.toFloat()
|
||||||
it.source_order = sourceOrder.toInt()
|
it.source_order = sourceOrder.toInt()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
package eu.kanade.domain.chapter.model
|
package eu.kanade.domain.chapter.model
|
||||||
|
|
||||||
import eu.kanade.domain.manga.model.downloadedFilter
|
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.DownloadManager
|
||||||
import eu.kanade.tachiyomi.data.download.model.Download
|
import eu.kanade.tachiyomi.ui.manga.ChapterList
|
||||||
import eu.kanade.tachiyomi.ui.manga.ChapterItem
|
|
||||||
import eu.kanade.tachiyomi.util.chapter.getChapterSort
|
|
||||||
import tachiyomi.domain.chapter.model.Chapter
|
import tachiyomi.domain.chapter.model.Chapter
|
||||||
|
import tachiyomi.domain.chapter.service.getChapterSort
|
||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
import tachiyomi.domain.manga.model.TriStateFilter
|
import tachiyomi.domain.manga.model.applyFilter
|
||||||
|
import tachiyomi.source.local.isLocal
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Applies the view filters to the list of chapters obtained from the database.
|
* Applies the view filters to the list of chapters obtained from the database.
|
||||||
@@ -20,30 +19,12 @@ fun List<Chapter>.applyFilters(manga: Manga, downloadManager: DownloadManager):
|
|||||||
val downloadedFilter = manga.downloadedFilter
|
val downloadedFilter = manga.downloadedFilter
|
||||||
val bookmarkedFilter = manga.bookmarkedFilter
|
val bookmarkedFilter = manga.bookmarkedFilter
|
||||||
|
|
||||||
return filter { chapter ->
|
return filter { chapter -> applyFilter(unreadFilter) { !chapter.read } }
|
||||||
when (unreadFilter) {
|
.filter { chapter -> applyFilter(bookmarkedFilter) { chapter.bookmark } }
|
||||||
TriStateFilter.DISABLED -> true
|
|
||||||
TriStateFilter.ENABLED_IS -> !chapter.read
|
|
||||||
TriStateFilter.ENABLED_NOT -> chapter.read
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.filter { chapter ->
|
.filter { chapter ->
|
||||||
when (bookmarkedFilter) {
|
applyFilter(downloadedFilter) {
|
||||||
TriStateFilter.DISABLED -> true
|
val downloaded = downloadManager.isChapterDownloaded(chapter.name, chapter.scanlator, manga.title, manga.source)
|
||||||
TriStateFilter.ENABLED_IS -> chapter.bookmark
|
downloaded || isLocalManga
|
||||||
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))
|
.sortedWith(getChapterSort(manga))
|
||||||
@@ -53,32 +34,14 @@ fun List<Chapter>.applyFilters(manga: Manga, downloadManager: DownloadManager):
|
|||||||
* Applies the view filters to the list of chapters obtained from the database.
|
* Applies the view filters to the list of chapters obtained from the database.
|
||||||
* @return an observable of the list of chapters filtered and sorted.
|
* @return an observable of the list of chapters filtered and sorted.
|
||||||
*/
|
*/
|
||||||
fun List<ChapterItem>.applyFilters(manga: Manga): Sequence<ChapterItem> {
|
fun List<ChapterList.Item>.applyFilters(manga: Manga): Sequence<ChapterList.Item> {
|
||||||
val isLocalManga = manga.isLocal()
|
val isLocalManga = manga.isLocal()
|
||||||
val unreadFilter = manga.unreadFilter
|
val unreadFilter = manga.unreadFilter
|
||||||
val downloadedFilter = manga.downloadedFilter
|
val downloadedFilter = manga.downloadedFilter
|
||||||
val bookmarkedFilter = manga.bookmarkedFilter
|
val bookmarkedFilter = manga.bookmarkedFilter
|
||||||
return asSequence()
|
return asSequence()
|
||||||
.filter { (chapter) ->
|
.filter { (chapter) -> applyFilter(unreadFilter) { !chapter.read } }
|
||||||
when (unreadFilter) {
|
.filter { (chapter) -> applyFilter(bookmarkedFilter) { chapter.bookmark } }
|
||||||
TriStateFilter.DISABLED -> true
|
.filter { applyFilter(downloadedFilter) { it.isDownloaded || isLocalManga } }
|
||||||
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) }
|
.sortedWith { (chapter1), (chapter2) -> getChapterSort(manga).invoke(chapter1, chapter2) }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
package eu.kanade.domain.download.interactor
|
package eu.kanade.domain.download.interactor
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
|
||||||
import tachiyomi.core.util.lang.withNonCancellableContext
|
import tachiyomi.core.util.lang.withNonCancellableContext
|
||||||
import tachiyomi.domain.chapter.model.Chapter
|
import tachiyomi.domain.chapter.model.Chapter
|
||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
|
import tachiyomi.domain.source.service.SourceManager
|
||||||
|
|
||||||
class DeleteDownload(
|
class DeleteDownload(
|
||||||
private val sourceManager: SourceManager,
|
private val sourceManager: SourceManager,
|
||||||
|
|||||||
@@ -1,111 +0,0 @@
|
|||||||
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", 24)
|
|
||||||
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
|
|
||||||
}
|
|
||||||
@@ -9,7 +9,7 @@ class SetMangaViewerFlags(
|
|||||||
private val mangaRepository: MangaRepository,
|
private val mangaRepository: MangaRepository,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
suspend fun awaitSetMangaReadingMode(id: Long, flag: Long) {
|
suspend fun awaitSetReadingMode(id: Long, flag: Long) {
|
||||||
val manga = mangaRepository.getMangaById(id)
|
val manga = mangaRepository.getMangaById(id)
|
||||||
mangaRepository.update(
|
mangaRepository.update(
|
||||||
MangaUpdate(
|
MangaUpdate(
|
||||||
@@ -19,7 +19,7 @@ class SetMangaViewerFlags(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun awaitSetOrientationType(id: Long, flag: Long) {
|
suspend fun awaitSetOrientation(id: Long, flag: Long) {
|
||||||
val manga = mangaRepository.getMangaById(id)
|
val manga = mangaRepository.getMangaById(id)
|
||||||
mangaRepository.update(
|
mangaRepository.update(
|
||||||
MangaUpdate(
|
MangaUpdate(
|
||||||
|
|||||||
@@ -1,18 +1,21 @@
|
|||||||
package eu.kanade.domain.manga.interactor
|
package eu.kanade.domain.manga.interactor
|
||||||
|
|
||||||
import eu.kanade.domain.manga.model.hasCustomCover
|
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.data.cache.CoverCache
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
|
import tachiyomi.domain.manga.interactor.FetchInterval
|
||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
import tachiyomi.domain.manga.model.MangaUpdate
|
import tachiyomi.domain.manga.model.MangaUpdate
|
||||||
import tachiyomi.domain.manga.repository.MangaRepository
|
import tachiyomi.domain.manga.repository.MangaRepository
|
||||||
|
import tachiyomi.source.local.isLocal
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
import java.time.ZonedDateTime
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
|
||||||
class UpdateManga(
|
class UpdateManga(
|
||||||
private val mangaRepository: MangaRepository,
|
private val mangaRepository: MangaRepository,
|
||||||
|
private val fetchInterval: FetchInterval,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
suspend fun await(mangaUpdate: MangaUpdate): Boolean {
|
suspend fun await(mangaUpdate: MangaUpdate): Boolean {
|
||||||
@@ -73,6 +76,16 @@ class UpdateManga(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun awaitUpdateFetchInterval(
|
||||||
|
manga: Manga,
|
||||||
|
dateTime: ZonedDateTime = ZonedDateTime.now(),
|
||||||
|
window: Pair<Long, Long> = fetchInterval.getWindow(dateTime),
|
||||||
|
): Boolean {
|
||||||
|
return fetchInterval.toMangaUpdateOrNull(manga, dateTime, window)
|
||||||
|
?.let { mangaRepository.update(it) }
|
||||||
|
?: false
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun awaitUpdateLastUpdate(mangaId: Long): Boolean {
|
suspend fun awaitUpdateLastUpdate(mangaId: Long): Boolean {
|
||||||
return mangaRepository.update(MangaUpdate(id = mangaId, lastUpdate = Date().time))
|
return mangaRepository.update(MangaUpdate(id = mangaId, lastUpdate = Date().time))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,12 +2,14 @@ package eu.kanade.domain.manga.model
|
|||||||
|
|
||||||
import eu.kanade.domain.base.BasePreferences
|
import eu.kanade.domain.base.BasePreferences
|
||||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||||
import eu.kanade.tachiyomi.source.LocalSource
|
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
|
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
|
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
|
||||||
|
import tachiyomi.core.metadata.comicinfo.ComicInfo
|
||||||
|
import tachiyomi.core.metadata.comicinfo.ComicInfoPublishingStatus
|
||||||
|
import tachiyomi.core.preference.TriState
|
||||||
|
import tachiyomi.domain.chapter.model.Chapter
|
||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
import tachiyomi.domain.manga.model.TriStateFilter
|
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
@@ -18,19 +20,19 @@ val Manga.readingModeType: Long
|
|||||||
val Manga.orientationType: Long
|
val Manga.orientationType: Long
|
||||||
get() = viewerFlags and OrientationType.MASK.toLong()
|
get() = viewerFlags and OrientationType.MASK.toLong()
|
||||||
|
|
||||||
val Manga.downloadedFilter: TriStateFilter
|
val Manga.downloadedFilter: TriState
|
||||||
get() {
|
get() {
|
||||||
if (forceDownloaded()) return TriStateFilter.ENABLED_IS
|
if (forceDownloaded()) return TriState.ENABLED_IS
|
||||||
return when (downloadedFilterRaw) {
|
return when (downloadedFilterRaw) {
|
||||||
Manga.CHAPTER_SHOW_DOWNLOADED -> TriStateFilter.ENABLED_IS
|
Manga.CHAPTER_SHOW_DOWNLOADED -> TriState.ENABLED_IS
|
||||||
Manga.CHAPTER_SHOW_NOT_DOWNLOADED -> TriStateFilter.ENABLED_NOT
|
Manga.CHAPTER_SHOW_NOT_DOWNLOADED -> TriState.ENABLED_NOT
|
||||||
else -> TriStateFilter.DISABLED
|
else -> TriState.DISABLED
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fun Manga.chaptersFiltered(): Boolean {
|
fun Manga.chaptersFiltered(): Boolean {
|
||||||
return unreadFilter != TriStateFilter.DISABLED ||
|
return unreadFilter != TriState.DISABLED ||
|
||||||
downloadedFilter != TriStateFilter.DISABLED ||
|
downloadedFilter != TriState.DISABLED ||
|
||||||
bookmarkedFilter != TriStateFilter.DISABLED
|
bookmarkedFilter != TriState.DISABLED
|
||||||
}
|
}
|
||||||
fun Manga.forceDownloaded(): Boolean {
|
fun Manga.forceDownloaded(): Boolean {
|
||||||
return favorite && Injekt.get<BasePreferences>().downloadedOnly().get()
|
return favorite && Injekt.get<BasePreferences>().downloadedOnly().get()
|
||||||
@@ -86,8 +88,36 @@ fun SManga.toDomainManga(sourceId: Long): Manga {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Manga.isLocal(): Boolean = source == LocalSource.ID
|
|
||||||
|
|
||||||
fun Manga.hasCustomCover(coverCache: CoverCache = Injekt.get()): Boolean {
|
fun Manga.hasCustomCover(coverCache: CoverCache = Injekt.get()): Boolean {
|
||||||
return coverCache.getCustomCoverFile(id).exists()
|
return coverCache.getCustomCoverFile(id).exists()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a ComicInfo instance based on the manga and chapter metadata.
|
||||||
|
*/
|
||||||
|
fun getComicInfo(manga: Manga, chapter: Chapter, chapterUrl: String, categories: List<String>?) = ComicInfo(
|
||||||
|
title = ComicInfo.Title(chapter.name),
|
||||||
|
series = ComicInfo.Series(manga.title),
|
||||||
|
number = chapter.chapterNumber.takeIf { it >= 0 }?.let {
|
||||||
|
if ((it.rem(1) == 0.0)) {
|
||||||
|
ComicInfo.Number(it.toInt().toString())
|
||||||
|
} else {
|
||||||
|
ComicInfo.Number(it.toString())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
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),
|
||||||
|
),
|
||||||
|
categories = categories?.let { ComicInfo.CategoriesTachiyomi(it.joinToString()) },
|
||||||
|
inker = null,
|
||||||
|
colorist = null,
|
||||||
|
letterer = null,
|
||||||
|
coverArtist = null,
|
||||||
|
tags = null,
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
package eu.kanade.domain.source.interactor
|
package eu.kanade.domain.source.interactor
|
||||||
|
|
||||||
import eu.kanade.domain.source.repository.SourceRepository
|
|
||||||
import eu.kanade.domain.source.service.SourcePreferences
|
import eu.kanade.domain.source.service.SourcePreferences
|
||||||
import eu.kanade.tachiyomi.source.LocalSource
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import tachiyomi.domain.source.model.Pin
|
import tachiyomi.domain.source.model.Pin
|
||||||
import tachiyomi.domain.source.model.Pins
|
import tachiyomi.domain.source.model.Pins
|
||||||
import tachiyomi.domain.source.model.Source
|
import tachiyomi.domain.source.model.Source
|
||||||
|
import tachiyomi.domain.source.repository.SourceRepository
|
||||||
|
import tachiyomi.source.local.isLocal
|
||||||
|
|
||||||
class GetEnabledSources(
|
class GetEnabledSources(
|
||||||
private val repository: SourceRepository,
|
private val repository: SourceRepository,
|
||||||
@@ -24,7 +24,7 @@ class GetEnabledSources(
|
|||||||
repository.getSources(),
|
repository.getSources(),
|
||||||
) { pinnedSourceIds, enabledLanguages, disabledSources, lastUsedSource, sources ->
|
) { pinnedSourceIds, enabledLanguages, disabledSources, lastUsedSource, sources ->
|
||||||
sources
|
sources
|
||||||
.filter { it.lang in enabledLanguages || it.id == LocalSource.ID }
|
.filter { it.lang in enabledLanguages || it.isLocal() }
|
||||||
.filterNot { it.id.toString() in disabledSources }
|
.filterNot { it.id.toString() in disabledSources }
|
||||||
.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name })
|
.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name })
|
||||||
.flatMap {
|
.flatMap {
|
||||||
|
|||||||
@@ -1,18 +1,19 @@
|
|||||||
package eu.kanade.domain.source.interactor
|
package eu.kanade.domain.source.interactor
|
||||||
|
|
||||||
import eu.kanade.domain.source.repository.SourceRepository
|
|
||||||
import eu.kanade.domain.source.service.SourcePreferences
|
import eu.kanade.domain.source.service.SourcePreferences
|
||||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import tachiyomi.domain.source.model.Source
|
import tachiyomi.domain.source.model.Source
|
||||||
|
import tachiyomi.domain.source.repository.SourceRepository
|
||||||
|
import java.util.SortedMap
|
||||||
|
|
||||||
class GetLanguagesWithSources(
|
class GetLanguagesWithSources(
|
||||||
private val repository: SourceRepository,
|
private val repository: SourceRepository,
|
||||||
private val preferences: SourcePreferences,
|
private val preferences: SourcePreferences,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun subscribe(): Flow<Map<String, List<Source>>> {
|
fun subscribe(): Flow<SortedMap<String, List<Source>>> {
|
||||||
return combine(
|
return combine(
|
||||||
preferences.enabledLanguages().changes(),
|
preferences.enabledLanguages().changes(),
|
||||||
preferences.disabledSources().changes(),
|
preferences.disabledSources().changes(),
|
||||||
@@ -23,7 +24,8 @@ class GetLanguagesWithSources(
|
|||||||
.thenBy(String.CASE_INSENSITIVE_ORDER) { it.name },
|
.thenBy(String.CASE_INSENSITIVE_ORDER) { it.name },
|
||||||
)
|
)
|
||||||
|
|
||||||
sortedSources.groupBy { it.lang }
|
sortedSources
|
||||||
|
.groupBy { it.lang }
|
||||||
.toSortedMap(
|
.toSortedMap(
|
||||||
compareBy<String> { it !in enabledLanguage }.then(LocaleHelper.comparator),
|
compareBy<String> { it !in enabledLanguage }.then(LocaleHelper.comparator),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
package eu.kanade.domain.source.interactor
|
package eu.kanade.domain.source.interactor
|
||||||
|
|
||||||
import eu.kanade.domain.source.repository.SourceRepository
|
|
||||||
import eu.kanade.domain.source.service.SourcePreferences
|
import eu.kanade.domain.source.service.SourcePreferences
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import tachiyomi.core.util.lang.compareToWithCollator
|
||||||
import tachiyomi.domain.source.model.Source
|
import tachiyomi.domain.source.model.Source
|
||||||
import java.text.Collator
|
import tachiyomi.domain.source.repository.SourceRepository
|
||||||
|
import tachiyomi.source.local.isLocal
|
||||||
import java.util.Collections
|
import java.util.Collections
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
class GetSourcesWithFavoriteCount(
|
class GetSourcesWithFavoriteCount(
|
||||||
private val repository: SourceRepository,
|
private val repository: SourceRepository,
|
||||||
@@ -20,7 +20,9 @@ class GetSourcesWithFavoriteCount(
|
|||||||
preferences.migrationSortingMode().changes(),
|
preferences.migrationSortingMode().changes(),
|
||||||
repository.getSourcesWithFavoriteCount(),
|
repository.getSourcesWithFavoriteCount(),
|
||||||
) { direction, mode, list ->
|
) { direction, mode, list ->
|
||||||
list.sortedWith(sortFn(direction, mode))
|
list
|
||||||
|
.filterNot { it.first.isLocal() }
|
||||||
|
.sortedWith(sortFn(direction, mode))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,17 +30,13 @@ class GetSourcesWithFavoriteCount(
|
|||||||
direction: SetMigrateSorting.Direction,
|
direction: SetMigrateSorting.Direction,
|
||||||
sorting: SetMigrateSorting.Mode,
|
sorting: SetMigrateSorting.Mode,
|
||||||
): java.util.Comparator<Pair<Source, Long>> {
|
): 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 ->
|
val sortFn: (Pair<Source, Long>, Pair<Source, Long>) -> Int = { a, b ->
|
||||||
when (sorting) {
|
when (sorting) {
|
||||||
SetMigrateSorting.Mode.ALPHABETICAL -> {
|
SetMigrateSorting.Mode.ALPHABETICAL -> {
|
||||||
when {
|
when {
|
||||||
a.first.isStub && b.first.isStub.not() -> -1
|
a.first.isStub && b.first.isStub.not() -> -1
|
||||||
b.first.isStub && a.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))
|
else -> a.first.name.lowercase().compareToWithCollator(b.first.name.lowercase())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SetMigrateSorting.Mode.TOTAL -> {
|
SetMigrateSorting.Mode.TOTAL -> {
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
package eu.kanade.domain.source.model
|
|
||||||
|
|
||||||
import androidx.paging.PagingSource
|
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
|
||||||
|
|
||||||
typealias SourcePagingSourceType = PagingSource<Long, SManga>
|
|
||||||
@@ -2,6 +2,7 @@ package eu.kanade.domain.source.service
|
|||||||
|
|
||||||
import eu.kanade.domain.source.interactor.SetMigrateSorting
|
import eu.kanade.domain.source.interactor.SetMigrateSorting
|
||||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||||
|
import tachiyomi.core.preference.Preference
|
||||||
import tachiyomi.core.preference.PreferenceStore
|
import tachiyomi.core.preference.PreferenceStore
|
||||||
import tachiyomi.core.preference.getEnum
|
import tachiyomi.core.preference.getEnum
|
||||||
import tachiyomi.domain.library.model.LibraryDisplayMode
|
import tachiyomi.domain.library.model.LibraryDisplayMode
|
||||||
@@ -18,7 +19,10 @@ class SourcePreferences(
|
|||||||
|
|
||||||
fun pinnedSources() = preferenceStore.getStringSet("pinned_catalogues", emptySet())
|
fun pinnedSources() = preferenceStore.getStringSet("pinned_catalogues", emptySet())
|
||||||
|
|
||||||
fun lastUsedSource() = preferenceStore.getLong("last_catalogue_source", -1)
|
fun lastUsedSource() = preferenceStore.getLong(
|
||||||
|
Preference.appStateKey("last_catalogue_source"),
|
||||||
|
-1,
|
||||||
|
)
|
||||||
|
|
||||||
fun showNsfwSource() = preferenceStore.getBoolean("show_nsfw_source", true)
|
fun showNsfwSource() = preferenceStore.getBoolean("show_nsfw_source", true)
|
||||||
|
|
||||||
@@ -28,7 +32,7 @@ class SourcePreferences(
|
|||||||
|
|
||||||
fun extensionUpdatesCount() = preferenceStore.getInt("ext_updates_count", 0)
|
fun extensionUpdatesCount() = preferenceStore.getInt("ext_updates_count", 0)
|
||||||
|
|
||||||
fun trustedSignatures() = preferenceStore.getStringSet("trusted_signatures", emptySet())
|
fun trustedSignatures() = preferenceStore.getStringSet(Preference.appStateKey("trusted_signatures"), emptySet())
|
||||||
|
|
||||||
fun searchPinnedSourcesOnly() = preferenceStore.getBoolean("search_pinned_sources_only", false)
|
fun hideInLibraryItems() = preferenceStore.getBoolean("browse_hide_in_library_items", false)
|
||||||
}
|
}
|
||||||
|
|||||||
104
app/src/main/java/eu/kanade/domain/track/interactor/AddTracks.kt
Normal file
104
app/src/main/java/eu/kanade/domain/track/interactor/AddTracks.kt
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
package eu.kanade.domain.track.interactor
|
||||||
|
|
||||||
|
import eu.kanade.domain.track.model.toDbTrack
|
||||||
|
import eu.kanade.domain.track.model.toDomainTrack
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
|
import eu.kanade.tachiyomi.data.track.EnhancedTracker
|
||||||
|
import eu.kanade.tachiyomi.data.track.Tracker
|
||||||
|
import eu.kanade.tachiyomi.source.Source
|
||||||
|
import eu.kanade.tachiyomi.util.lang.convertEpochMillisZone
|
||||||
|
import logcat.LogPriority
|
||||||
|
import tachiyomi.core.util.lang.withIOContext
|
||||||
|
import tachiyomi.core.util.lang.withNonCancellableContext
|
||||||
|
import tachiyomi.core.util.system.logcat
|
||||||
|
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
|
||||||
|
import tachiyomi.domain.history.interactor.GetHistory
|
||||||
|
import tachiyomi.domain.manga.model.Manga
|
||||||
|
import tachiyomi.domain.track.interactor.GetTracks
|
||||||
|
import tachiyomi.domain.track.interactor.InsertTrack
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
import java.time.ZoneOffset
|
||||||
|
|
||||||
|
class AddTracks(
|
||||||
|
private val getTracks: GetTracks,
|
||||||
|
private val insertTrack: InsertTrack,
|
||||||
|
private val syncChapterProgressWithTrack: SyncChapterProgressWithTrack,
|
||||||
|
private val getChaptersByMangaId: GetChaptersByMangaId,
|
||||||
|
) {
|
||||||
|
|
||||||
|
// TODO: update all trackers based on common data
|
||||||
|
suspend fun bind(tracker: Tracker, item: Track, mangaId: Long) = withNonCancellableContext {
|
||||||
|
withIOContext {
|
||||||
|
val allChapters = getChaptersByMangaId.await(mangaId)
|
||||||
|
val hasReadChapters = allChapters.any { it.read }
|
||||||
|
tracker.bind(item, hasReadChapters)
|
||||||
|
|
||||||
|
var track = item.toDomainTrack(idRequired = false) ?: return@withIOContext
|
||||||
|
|
||||||
|
insertTrack.await(track)
|
||||||
|
|
||||||
|
// TODO: merge into [SyncChapterProgressWithTrack]?
|
||||||
|
// Update chapter progress if newer chapters marked read locally
|
||||||
|
if (hasReadChapters) {
|
||||||
|
val latestLocalReadChapterNumber = allChapters
|
||||||
|
.sortedBy { it.chapterNumber }
|
||||||
|
.takeWhile { it.read }
|
||||||
|
.lastOrNull()
|
||||||
|
?.chapterNumber ?: -1.0
|
||||||
|
|
||||||
|
if (latestLocalReadChapterNumber > track.lastChapterRead) {
|
||||||
|
track = track.copy(
|
||||||
|
lastChapterRead = latestLocalReadChapterNumber,
|
||||||
|
)
|
||||||
|
tracker.setRemoteLastChapterRead(track.toDbTrack(), latestLocalReadChapterNumber.toInt())
|
||||||
|
}
|
||||||
|
|
||||||
|
if (track.startDate <= 0) {
|
||||||
|
val firstReadChapterDate = Injekt.get<GetHistory>().await(mangaId)
|
||||||
|
.sortedBy { it.readAt }
|
||||||
|
.firstOrNull()
|
||||||
|
?.readAt
|
||||||
|
|
||||||
|
firstReadChapterDate?.let {
|
||||||
|
val startDate = firstReadChapterDate.time.convertEpochMillisZone(ZoneOffset.systemDefault(), ZoneOffset.UTC)
|
||||||
|
track = track.copy(
|
||||||
|
startDate = startDate,
|
||||||
|
)
|
||||||
|
tracker.setRemoteStartDate(track.toDbTrack(), startDate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
syncChapterProgressWithTrack.await(mangaId, track, tracker)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun bindEnhancedTrackers(manga: Manga, source: Source) = withNonCancellableContext {
|
||||||
|
withIOContext {
|
||||||
|
getTracks.await(manga.id)
|
||||||
|
.filterIsInstance<EnhancedTracker>()
|
||||||
|
.filter { it.accept(source) }
|
||||||
|
.forEach { service ->
|
||||||
|
try {
|
||||||
|
service.match(manga)?.let { track ->
|
||||||
|
track.manga_id = manga.id
|
||||||
|
(service as Tracker).bind(track)
|
||||||
|
insertTrack.await(track.toDomainTrack()!!)
|
||||||
|
|
||||||
|
syncChapterProgressWithTrack.await(
|
||||||
|
manga.id,
|
||||||
|
track.toDomainTrack()!!,
|
||||||
|
service,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logcat(
|
||||||
|
LogPriority.WARN,
|
||||||
|
e,
|
||||||
|
) { "Could not match manga: ${manga.title} with service $service" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package eu.kanade.domain.track.interactor
|
||||||
|
|
||||||
|
import eu.kanade.domain.track.model.toDbTrack
|
||||||
|
import eu.kanade.domain.track.model.toDomainTrack
|
||||||
|
import eu.kanade.tachiyomi.data.track.Tracker
|
||||||
|
import eu.kanade.tachiyomi.data.track.TrackerManager
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.awaitAll
|
||||||
|
import kotlinx.coroutines.supervisorScope
|
||||||
|
import tachiyomi.domain.track.interactor.GetTracks
|
||||||
|
import tachiyomi.domain.track.interactor.InsertTrack
|
||||||
|
|
||||||
|
class RefreshTracks(
|
||||||
|
private val getTracks: GetTracks,
|
||||||
|
private val trackerManager: TrackerManager,
|
||||||
|
private val insertTrack: InsertTrack,
|
||||||
|
private val syncChapterProgressWithTrack: SyncChapterProgressWithTrack,
|
||||||
|
) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches updated tracking data from all logged in trackers.
|
||||||
|
*
|
||||||
|
* @return Failed updates.
|
||||||
|
*/
|
||||||
|
suspend fun await(mangaId: Long): List<Pair<Tracker?, Throwable>> {
|
||||||
|
return supervisorScope {
|
||||||
|
return@supervisorScope getTracks.await(mangaId)
|
||||||
|
.map { it to trackerManager.get(it.syncId) }
|
||||||
|
.filter { (_, service) -> service?.isLoggedIn == true }
|
||||||
|
.map { (track, service) ->
|
||||||
|
async {
|
||||||
|
return@async try {
|
||||||
|
val updatedTrack = service!!.refresh(track.toDbTrack())
|
||||||
|
insertTrack.await(updatedTrack.toDomainTrack()!!)
|
||||||
|
syncChapterProgressWithTrack.await(mangaId, track, service)
|
||||||
|
null
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
service to e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.awaitAll()
|
||||||
|
.filterNotNull()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,27 +1,35 @@
|
|||||||
package eu.kanade.domain.chapter.interactor
|
package eu.kanade.domain.track.interactor
|
||||||
|
|
||||||
import eu.kanade.domain.track.interactor.InsertTrack
|
|
||||||
import eu.kanade.domain.track.model.toDbTrack
|
import eu.kanade.domain.track.model.toDbTrack
|
||||||
import eu.kanade.tachiyomi.data.track.TrackService
|
import eu.kanade.tachiyomi.data.track.EnhancedTracker
|
||||||
|
import eu.kanade.tachiyomi.data.track.Tracker
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import tachiyomi.core.util.system.logcat
|
import tachiyomi.core.util.system.logcat
|
||||||
import tachiyomi.domain.chapter.model.Chapter
|
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
|
||||||
|
import tachiyomi.domain.chapter.interactor.UpdateChapter
|
||||||
import tachiyomi.domain.chapter.model.toChapterUpdate
|
import tachiyomi.domain.chapter.model.toChapterUpdate
|
||||||
|
import tachiyomi.domain.track.interactor.InsertTrack
|
||||||
import tachiyomi.domain.track.model.Track
|
import tachiyomi.domain.track.model.Track
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
|
|
||||||
class SyncChaptersWithTrackServiceTwoWay(
|
class SyncChapterProgressWithTrack(
|
||||||
private val updateChapter: UpdateChapter = Injekt.get(),
|
private val updateChapter: UpdateChapter,
|
||||||
private val insertTrack: InsertTrack = Injekt.get(),
|
private val insertTrack: InsertTrack,
|
||||||
|
private val getChaptersByMangaId: GetChaptersByMangaId,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
suspend fun await(
|
suspend fun await(
|
||||||
chapters: List<Chapter>,
|
mangaId: Long,
|
||||||
remoteTrack: Track,
|
remoteTrack: Track,
|
||||||
service: TrackService,
|
tracker: Tracker,
|
||||||
) {
|
) {
|
||||||
val sortedChapters = chapters.sortedBy { it.chapterNumber }
|
if (tracker !is EnhancedTracker) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val sortedChapters = getChaptersByMangaId.await(mangaId)
|
||||||
|
.sortedBy { it.chapterNumber }
|
||||||
|
.filter { it.isRecognizedNumber }
|
||||||
|
|
||||||
val chapterUpdates = sortedChapters
|
val chapterUpdates = sortedChapters
|
||||||
.filter { chapter -> chapter.chapterNumber <= remoteTrack.lastChapterRead && !chapter.read }
|
.filter { chapter -> chapter.chapterNumber <= remoteTrack.lastChapterRead && !chapter.read }
|
||||||
.map { it.copy(read = true).toChapterUpdate() }
|
.map { it.copy(read = true).toChapterUpdate() }
|
||||||
@@ -31,7 +39,7 @@ class SyncChaptersWithTrackServiceTwoWay(
|
|||||||
val updatedTrack = remoteTrack.copy(lastChapterRead = localLastRead.toDouble())
|
val updatedTrack = remoteTrack.copy(lastChapterRead = localLastRead.toDouble())
|
||||||
|
|
||||||
try {
|
try {
|
||||||
service.update(updatedTrack.toDbTrack())
|
tracker.update(updatedTrack.toDbTrack())
|
||||||
updateChapter.awaitAll(chapterUpdates)
|
updateChapter.awaitAll(chapterUpdates)
|
||||||
insertTrack.await(updatedTrack)
|
insertTrack.await(updatedTrack)
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package eu.kanade.domain.track.interactor
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import eu.kanade.domain.track.model.toDbTrack
|
||||||
|
import eu.kanade.domain.track.model.toDomainTrack
|
||||||
|
import eu.kanade.domain.track.service.DelayedTrackingUpdateJob
|
||||||
|
import eu.kanade.domain.track.store.DelayedTrackingStore
|
||||||
|
import eu.kanade.tachiyomi.data.track.TrackerManager
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.awaitAll
|
||||||
|
import logcat.LogPriority
|
||||||
|
import tachiyomi.core.util.lang.withNonCancellableContext
|
||||||
|
import tachiyomi.core.util.system.logcat
|
||||||
|
import tachiyomi.domain.track.interactor.GetTracks
|
||||||
|
import tachiyomi.domain.track.interactor.InsertTrack
|
||||||
|
|
||||||
|
class TrackChapter(
|
||||||
|
private val getTracks: GetTracks,
|
||||||
|
private val trackerManager: TrackerManager,
|
||||||
|
private val insertTrack: InsertTrack,
|
||||||
|
private val delayedTrackingStore: DelayedTrackingStore,
|
||||||
|
) {
|
||||||
|
|
||||||
|
suspend fun await(context: Context, mangaId: Long, chapterNumber: Double) {
|
||||||
|
withNonCancellableContext {
|
||||||
|
val tracks = getTracks.await(mangaId)
|
||||||
|
if (tracks.isEmpty()) return@withNonCancellableContext
|
||||||
|
|
||||||
|
tracks.mapNotNull { track ->
|
||||||
|
val service = trackerManager.get(track.syncId)
|
||||||
|
if (service == null || !service.isLoggedIn || chapterNumber <= track.lastChapterRead) {
|
||||||
|
return@mapNotNull null
|
||||||
|
}
|
||||||
|
|
||||||
|
async {
|
||||||
|
runCatching {
|
||||||
|
try {
|
||||||
|
val updatedTrack = service.refresh(track.toDbTrack())
|
||||||
|
.toDomainTrack(idRequired = true)!!
|
||||||
|
.copy(lastChapterRead = chapterNumber)
|
||||||
|
service.update(updatedTrack.toDbTrack(), true)
|
||||||
|
insertTrack.await(updatedTrack)
|
||||||
|
delayedTrackingStore.remove(track.id)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
delayedTrackingStore.add(track.id, chapterNumber)
|
||||||
|
DelayedTrackingUpdateJob.setupTask(context)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.awaitAll()
|
||||||
|
.mapNotNull { it.exceptionOrNull() }
|
||||||
|
.forEach { logcat(LogPriority.WARN, it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,7 +22,7 @@ fun Track.toDbTrack(): DbTrack = DbTrack.create(syncId).also {
|
|||||||
it.last_chapter_read = lastChapterRead.toFloat()
|
it.last_chapter_read = lastChapterRead.toFloat()
|
||||||
it.total_chapters = totalChapters.toInt()
|
it.total_chapters = totalChapters.toInt()
|
||||||
it.status = status.toInt()
|
it.status = status.toInt()
|
||||||
it.score = score
|
it.score = score.toFloat()
|
||||||
it.tracking_url = remoteUrl
|
it.tracking_url = remoteUrl
|
||||||
it.started_reading_date = startDate
|
it.started_reading_date = startDate
|
||||||
it.finished_reading_date = finishDate
|
it.finished_reading_date = finishDate
|
||||||
@@ -40,7 +40,7 @@ fun DbTrack.toDomainTrack(idRequired: Boolean = true): Track? {
|
|||||||
lastChapterRead = last_chapter_read.toDouble(),
|
lastChapterRead = last_chapter_read.toDouble(),
|
||||||
totalChapters = total_chapters.toLong(),
|
totalChapters = total_chapters.toLong(),
|
||||||
status = status.toLong(),
|
status = status.toLong(),
|
||||||
score = score,
|
score = score.toDouble(),
|
||||||
remoteUrl = tracking_url,
|
remoteUrl = tracking_url,
|
||||||
startDate = started_reading_date,
|
startDate = started_reading_date,
|
||||||
finishDate = finished_reading_date,
|
finishDate = finished_reading_date,
|
||||||
|
|||||||
@@ -7,72 +7,65 @@ import androidx.work.CoroutineWorker
|
|||||||
import androidx.work.ExistingWorkPolicy
|
import androidx.work.ExistingWorkPolicy
|
||||||
import androidx.work.NetworkType
|
import androidx.work.NetworkType
|
||||||
import androidx.work.OneTimeWorkRequestBuilder
|
import androidx.work.OneTimeWorkRequestBuilder
|
||||||
import androidx.work.WorkManager
|
|
||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
import eu.kanade.domain.track.interactor.GetTracks
|
import eu.kanade.domain.track.interactor.TrackChapter
|
||||||
import eu.kanade.domain.track.interactor.InsertTrack
|
|
||||||
import eu.kanade.domain.track.model.toDbTrack
|
|
||||||
import eu.kanade.domain.track.store.DelayedTrackingStore
|
import eu.kanade.domain.track.store.DelayedTrackingStore
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.util.system.workManager
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import tachiyomi.core.util.lang.withIOContext
|
import tachiyomi.core.util.lang.withIOContext
|
||||||
import tachiyomi.core.util.system.logcat
|
import tachiyomi.core.util.system.logcat
|
||||||
|
import tachiyomi.domain.track.interactor.GetTracks
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.util.concurrent.TimeUnit
|
import kotlin.time.Duration.Companion.minutes
|
||||||
|
import kotlin.time.toJavaDuration
|
||||||
|
|
||||||
class DelayedTrackingUpdateJob(context: Context, workerParams: WorkerParameters) :
|
class DelayedTrackingUpdateJob(private val context: Context, workerParams: WorkerParameters) :
|
||||||
CoroutineWorker(context, workerParams) {
|
CoroutineWorker(context, workerParams) {
|
||||||
|
|
||||||
override suspend fun doWork(): Result {
|
override suspend fun doWork(): Result {
|
||||||
val getTracks = Injekt.get<GetTracks>()
|
if (runAttemptCount > 3) {
|
||||||
val insertTrack = Injekt.get<InsertTrack>()
|
return Result.failure()
|
||||||
|
}
|
||||||
|
|
||||||
|
val getTracks = Injekt.get<GetTracks>()
|
||||||
|
val trackChapter = Injekt.get<TrackChapter>()
|
||||||
|
|
||||||
val trackManager = Injekt.get<TrackManager>()
|
|
||||||
val delayedTrackingStore = Injekt.get<DelayedTrackingStore>()
|
val delayedTrackingStore = Injekt.get<DelayedTrackingStore>()
|
||||||
|
|
||||||
withIOContext {
|
withIOContext {
|
||||||
val tracks = delayedTrackingStore.getItems().mapNotNull {
|
delayedTrackingStore.getItems()
|
||||||
val track = getTracks.awaitOne(it.trackId)
|
.mapNotNull {
|
||||||
if (track == null) {
|
val track = getTracks.awaitOne(it.trackId)
|
||||||
delayedTrackingStore.remove(it.trackId)
|
if (track == null) {
|
||||||
}
|
delayedTrackingStore.remove(it.trackId)
|
||||||
track
|
|
||||||
}
|
|
||||||
|
|
||||||
tracks.forEach { track ->
|
|
||||||
try {
|
|
||||||
val service = trackManager.getService(track.syncId)
|
|
||||||
if (service != null && service.isLogged) {
|
|
||||||
service.update(track.toDbTrack(), true)
|
|
||||||
insertTrack.await(track)
|
|
||||||
}
|
}
|
||||||
delayedTrackingStore.remove(track.id)
|
track?.copy(lastChapterRead = it.lastChapterRead.toDouble())
|
||||||
} catch (e: Exception) {
|
}
|
||||||
logcat(LogPriority.ERROR, e)
|
.forEach { track ->
|
||||||
|
logcat(LogPriority.DEBUG) { "Updating delayed track item: ${track.mangaId}, last chapter read: ${track.lastChapterRead}" }
|
||||||
|
trackChapter.await(context, track.mangaId, track.lastChapterRead)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Result.success()
|
return if (delayedTrackingStore.getItems().isEmpty()) Result.success() else Result.retry()
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "DelayedTrackingUpdate"
|
private const val TAG = "DelayedTrackingUpdate"
|
||||||
|
|
||||||
fun setupTask(context: Context) {
|
fun setupTask(context: Context) {
|
||||||
val constraints = Constraints.Builder()
|
val constraints = Constraints(
|
||||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
requiredNetworkType = NetworkType.CONNECTED,
|
||||||
.build()
|
)
|
||||||
|
|
||||||
val request = OneTimeWorkRequestBuilder<DelayedTrackingUpdateJob>()
|
val request = OneTimeWorkRequestBuilder<DelayedTrackingUpdateJob>()
|
||||||
.setConstraints(constraints)
|
.setConstraints(constraints)
|
||||||
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 20, TimeUnit.SECONDS)
|
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 5.minutes.toJavaDuration())
|
||||||
.addTag(TAG)
|
.addTag(TAG)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
WorkManager.getInstance(context)
|
context.workManager.enqueueUniqueWork(TAG, ExistingWorkPolicy.REPLACE, request)
|
||||||
.enqueueUniqueWork(TAG, ExistingWorkPolicy.REPLACE, request)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,33 +1,34 @@
|
|||||||
package eu.kanade.domain.track.service
|
package eu.kanade.domain.track.service
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.data.track.TrackService
|
import eu.kanade.tachiyomi.data.track.Tracker
|
||||||
import eu.kanade.tachiyomi.data.track.anilist.Anilist
|
import eu.kanade.tachiyomi.data.track.anilist.Anilist
|
||||||
|
import tachiyomi.core.preference.Preference
|
||||||
import tachiyomi.core.preference.PreferenceStore
|
import tachiyomi.core.preference.PreferenceStore
|
||||||
|
|
||||||
class TrackPreferences(
|
class TrackPreferences(
|
||||||
private val preferenceStore: PreferenceStore,
|
private val preferenceStore: PreferenceStore,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun trackUsername(sync: TrackService) = preferenceStore.getString(trackUsername(sync.id), "")
|
fun trackUsername(sync: Tracker) = preferenceStore.getString(trackUsername(sync.id), "")
|
||||||
|
|
||||||
fun trackPassword(sync: TrackService) = preferenceStore.getString(trackPassword(sync.id), "")
|
fun trackPassword(sync: Tracker) = preferenceStore.getString(trackPassword(sync.id), "")
|
||||||
|
|
||||||
fun setTrackCredentials(sync: TrackService, username: String, password: String) {
|
fun setCredentials(sync: Tracker, username: String, password: String) {
|
||||||
trackUsername(sync).set(username)
|
trackUsername(sync).set(username)
|
||||||
trackPassword(sync).set(password)
|
trackPassword(sync).set(password)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun trackToken(sync: TrackService) = preferenceStore.getString(trackToken(sync.id), "")
|
fun trackToken(sync: Tracker) = preferenceStore.getString(trackToken(sync.id), "")
|
||||||
|
|
||||||
fun anilistScoreType() = preferenceStore.getString("anilist_score_type", Anilist.POINT_10)
|
fun anilistScoreType() = preferenceStore.getString("anilist_score_type", Anilist.POINT_10)
|
||||||
|
|
||||||
fun autoUpdateTrack() = preferenceStore.getBoolean("pref_auto_update_manga_sync_key", true)
|
fun autoUpdateTrack() = preferenceStore.getBoolean("pref_auto_update_manga_sync_key", true)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun trackUsername(syncId: Long) = "pref_mangasync_username_$syncId"
|
fun trackUsername(syncId: Long) = Preference.privateKey("pref_mangasync_username_$syncId")
|
||||||
|
|
||||||
private fun trackPassword(syncId: Long) = "pref_mangasync_password_$syncId"
|
private fun trackPassword(syncId: Long) = Preference.privateKey("pref_mangasync_password_$syncId")
|
||||||
|
|
||||||
private fun trackToken(syncId: Long) = "track_token_$syncId"
|
private fun trackToken(syncId: Long) = Preference.privateKey("track_token_$syncId")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import android.content.Context
|
|||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import tachiyomi.core.util.system.logcat
|
import tachiyomi.core.util.system.logcat
|
||||||
import tachiyomi.domain.track.model.Track
|
|
||||||
|
|
||||||
class DelayedTrackingStore(context: Context) {
|
class DelayedTrackingStore(context: Context) {
|
||||||
|
|
||||||
@@ -13,13 +12,12 @@ class DelayedTrackingStore(context: Context) {
|
|||||||
*/
|
*/
|
||||||
private val preferences = context.getSharedPreferences("tracking_queue", Context.MODE_PRIVATE)
|
private val preferences = context.getSharedPreferences("tracking_queue", Context.MODE_PRIVATE)
|
||||||
|
|
||||||
fun addItem(track: Track) {
|
fun add(trackId: Long, lastChapterRead: Double) {
|
||||||
val trackId = track.id.toString()
|
val previousLastChapterRead = preferences.getFloat(trackId.toString(), 0f)
|
||||||
val lastChapterRead = preferences.getFloat(trackId, 0f)
|
if (lastChapterRead > previousLastChapterRead) {
|
||||||
if (track.lastChapterRead > lastChapterRead) {
|
logcat(LogPriority.DEBUG) { "Queuing track item: $trackId, last chapter read: $lastChapterRead" }
|
||||||
logcat(LogPriority.DEBUG) { "Queuing track item: $trackId, last chapter read: ${track.lastChapterRead}" }
|
|
||||||
preferences.edit {
|
preferences.edit {
|
||||||
putFloat(trackId, track.lastChapterRead.toFloat())
|
putFloat(trackId.toString(), lastChapterRead.toFloat())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package eu.kanade.domain.ui
|
package eu.kanade.domain.ui
|
||||||
|
|
||||||
import android.os.Build
|
|
||||||
import eu.kanade.domain.ui.model.AppTheme
|
import eu.kanade.domain.ui.model.AppTheme
|
||||||
import eu.kanade.domain.ui.model.TabletUiMode
|
import eu.kanade.domain.ui.model.TabletUiMode
|
||||||
import eu.kanade.domain.ui.model.ThemeMode
|
import eu.kanade.domain.ui.model.ThemeMode
|
||||||
@@ -16,10 +15,7 @@ class UiPreferences(
|
|||||||
private val preferenceStore: PreferenceStore,
|
private val preferenceStore: PreferenceStore,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun themeMode() = preferenceStore.getEnum(
|
fun themeMode() = preferenceStore.getEnum("pref_theme_mode_key", ThemeMode.SYSTEM)
|
||||||
"pref_theme_mode_key",
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { ThemeMode.SYSTEM } else { ThemeMode.LIGHT },
|
|
||||||
)
|
|
||||||
|
|
||||||
fun appTheme() = preferenceStore.getEnum(
|
fun appTheme() = preferenceStore.getEnum(
|
||||||
"pref_app_theme",
|
"pref_app_theme",
|
||||||
@@ -28,7 +24,7 @@ class UiPreferences(
|
|||||||
|
|
||||||
fun themeDarkAmoled() = preferenceStore.getBoolean("pref_theme_dark_amoled_key", false)
|
fun themeDarkAmoled() = preferenceStore.getBoolean("pref_theme_dark_amoled_key", false)
|
||||||
|
|
||||||
fun relativeTime() = preferenceStore.getInt("relative_time", 7)
|
fun relativeTime() = preferenceStore.getBoolean("relative_time_v2", true)
|
||||||
|
|
||||||
fun dateFormat() = preferenceStore.getString("app_date_format", "")
|
fun dateFormat() = preferenceStore.getString("app_date_format", "")
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package eu.kanade.presentation.browse
|
package eu.kanade.presentation.browse
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.grid.GridCells
|
import androidx.compose.foundation.lazy.grid.GridCells
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.outlined.HelpOutline
|
import androidx.compose.material.icons.outlined.HelpOutline
|
||||||
@@ -11,26 +12,31 @@ import androidx.compose.material3.SnackbarHostState
|
|||||||
import androidx.compose.material3.SnackbarResult
|
import androidx.compose.material3.SnackbarResult
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.paging.LoadState
|
import androidx.paging.LoadState
|
||||||
import androidx.paging.compose.LazyPagingItems
|
import androidx.paging.compose.LazyPagingItems
|
||||||
import eu.kanade.data.source.NoResultsException
|
|
||||||
import eu.kanade.presentation.browse.components.BrowseSourceComfortableGrid
|
import eu.kanade.presentation.browse.components.BrowseSourceComfortableGrid
|
||||||
import eu.kanade.presentation.browse.components.BrowseSourceCompactGrid
|
import eu.kanade.presentation.browse.components.BrowseSourceCompactGrid
|
||||||
import eu.kanade.presentation.browse.components.BrowseSourceList
|
import eu.kanade.presentation.browse.components.BrowseSourceList
|
||||||
import eu.kanade.presentation.components.EmptyScreen
|
import eu.kanade.presentation.components.AppBar
|
||||||
import eu.kanade.presentation.components.EmptyScreenAction
|
import eu.kanade.presentation.util.formattedMessage
|
||||||
import eu.kanade.presentation.components.LoadingScreen
|
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.source.LocalSource
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import tachiyomi.domain.library.model.LibraryDisplayMode
|
import tachiyomi.domain.library.model.LibraryDisplayMode
|
||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
|
import tachiyomi.domain.source.model.StubSource
|
||||||
|
import tachiyomi.presentation.core.components.material.Scaffold
|
||||||
|
import tachiyomi.presentation.core.screens.EmptyScreen
|
||||||
|
import tachiyomi.presentation.core.screens.EmptyScreenAction
|
||||||
|
import tachiyomi.presentation.core.screens.LoadingScreen
|
||||||
|
import tachiyomi.source.local.LocalSource
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun BrowseSourceContent(
|
fun BrowseSourceContent(
|
||||||
source: CatalogueSource?,
|
source: Source?,
|
||||||
mangaList: LazyPagingItems<StateFlow<Manga>>,
|
mangaList: LazyPagingItems<StateFlow<Manga>>,
|
||||||
columns: GridCells,
|
columns: GridCells,
|
||||||
displayMode: LibraryDisplayMode,
|
displayMode: LibraryDisplayMode,
|
||||||
@@ -48,30 +54,26 @@ fun BrowseSourceContent(
|
|||||||
?: mangaList.loadState.append.takeIf { it is LoadState.Error }
|
?: mangaList.loadState.append.takeIf { it is LoadState.Error }
|
||||||
|
|
||||||
val getErrorMessage: (LoadState.Error) -> String = { state ->
|
val getErrorMessage: (LoadState.Error) -> String = { state ->
|
||||||
when {
|
with(context) { state.error.formattedMessage }
|
||||||
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) {
|
LaunchedEffect(errorState) {
|
||||||
if (mangaList.itemCount > 0 && errorState != null && errorState is LoadState.Error) {
|
if (mangaList.itemCount > 0 && errorState != null && errorState is LoadState.Error) {
|
||||||
val result = snackbarHostState.showSnackbar(
|
val result = snackbarHostState.showSnackbar(
|
||||||
message = getErrorMessage(errorState),
|
message = getErrorMessage(errorState),
|
||||||
actionLabel = context.getString(R.string.action_webview_refresh),
|
actionLabel = context.getString(R.string.action_retry),
|
||||||
duration = SnackbarDuration.Indefinite,
|
duration = SnackbarDuration.Indefinite,
|
||||||
)
|
)
|
||||||
when (result) {
|
when (result) {
|
||||||
SnackbarResult.Dismissed -> snackbarHostState.currentSnackbarData?.dismiss()
|
SnackbarResult.Dismissed -> snackbarHostState.currentSnackbarData?.dismiss()
|
||||||
SnackbarResult.ActionPerformed -> mangaList.refresh()
|
SnackbarResult.ActionPerformed -> mangaList.retry()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mangaList.itemCount <= 0 && errorState != null && errorState is LoadState.Error) {
|
if (mangaList.itemCount <= 0 && errorState != null && errorState is LoadState.Error) {
|
||||||
EmptyScreen(
|
EmptyScreen(
|
||||||
|
modifier = Modifier.padding(contentPadding),
|
||||||
message = getErrorMessage(errorState),
|
message = getErrorMessage(errorState),
|
||||||
actions = if (source is LocalSource) {
|
actions = if (source is LocalSource) {
|
||||||
listOf(
|
listOf(
|
||||||
@@ -106,7 +108,9 @@ fun BrowseSourceContent(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (mangaList.itemCount == 0 && mangaList.loadState.refresh is LoadState.Loading) {
|
if (mangaList.itemCount == 0 && mangaList.loadState.refresh is LoadState.Loading) {
|
||||||
LoadingScreen()
|
LoadingScreen(
|
||||||
|
modifier = Modifier.padding(contentPadding),
|
||||||
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,3 +143,24 @@ fun BrowseSourceContent(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MissingSourceScreen(
|
||||||
|
source: StubSource,
|
||||||
|
navigateUp: () -> Unit,
|
||||||
|
) {
|
||||||
|
Scaffold(
|
||||||
|
topBar = { scrollBehavior ->
|
||||||
|
AppBar(
|
||||||
|
title = source.name,
|
||||||
|
navigateUp = navigateUp,
|
||||||
|
scrollBehavior = scrollBehavior,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
) { paddingValues ->
|
||||||
|
EmptyScreen(
|
||||||
|
message = stringResource(R.string.source_not_installed, source.toString()),
|
||||||
|
modifier = Modifier.padding(paddingValues),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,12 +10,10 @@ import androidx.compose.foundation.layout.Arrangement
|
|||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.outlined.HelpOutline
|
import androidx.compose.material.icons.outlined.HelpOutline
|
||||||
@@ -23,6 +21,7 @@ import androidx.compose.material.icons.outlined.History
|
|||||||
import androidx.compose.material.icons.outlined.Settings
|
import androidx.compose.material.icons.outlined.Settings
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
@@ -30,6 +29,7 @@ import androidx.compose.material3.OutlinedButton
|
|||||||
import androidx.compose.material3.Switch
|
import androidx.compose.material3.Switch
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.material3.VerticalDivider
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
@@ -47,25 +47,23 @@ import eu.kanade.domain.extension.interactor.ExtensionSourceItem
|
|||||||
import eu.kanade.presentation.browse.components.ExtensionIcon
|
import eu.kanade.presentation.browse.components.ExtensionIcon
|
||||||
import eu.kanade.presentation.components.AppBar
|
import eu.kanade.presentation.components.AppBar
|
||||||
import eu.kanade.presentation.components.AppBarActions
|
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.components.WarningBanner
|
||||||
import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget
|
import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget
|
||||||
import eu.kanade.presentation.more.settings.widget.TrailingWidgetBuffer
|
import eu.kanade.presentation.more.settings.widget.TrailingWidgetBuffer
|
||||||
import eu.kanade.presentation.util.padding
|
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.extension.model.Extension
|
import eu.kanade.tachiyomi.extension.model.Extension
|
||||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||||
import eu.kanade.tachiyomi.ui.browse.extension.details.ExtensionDetailsState
|
import eu.kanade.tachiyomi.ui.browse.extension.details.ExtensionDetailsScreenModel
|
||||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||||
|
import tachiyomi.presentation.core.components.ScrollbarLazyColumn
|
||||||
|
import tachiyomi.presentation.core.components.material.Scaffold
|
||||||
|
import tachiyomi.presentation.core.components.material.padding
|
||||||
|
import tachiyomi.presentation.core.screens.EmptyScreen
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ExtensionDetailsScreen(
|
fun ExtensionDetailsScreen(
|
||||||
navigateUp: () -> Unit,
|
navigateUp: () -> Unit,
|
||||||
state: ExtensionDetailsState,
|
state: ExtensionDetailsScreenModel.State,
|
||||||
onClickSourcePreferences: (sourceId: Long) -> Unit,
|
onClickSourcePreferences: (sourceId: Long) -> Unit,
|
||||||
onClickWhatsNew: () -> Unit,
|
onClickWhatsNew: () -> Unit,
|
||||||
onClickReadme: () -> Unit,
|
onClickReadme: () -> Unit,
|
||||||
@@ -176,7 +174,8 @@ private fun ExtensionDetails(
|
|||||||
data = Uri.fromParts("package", extension.pkgName, null)
|
data = Uri.fromParts("package", extension.pkgName, null)
|
||||||
context.startActivity(this)
|
context.startActivity(this)
|
||||||
}
|
}
|
||||||
},
|
Unit
|
||||||
|
}.takeIf { extension.isShared },
|
||||||
onClickAgeRating = {
|
onClickAgeRating = {
|
||||||
showNsfwWarning = true
|
showNsfwWarning = true
|
||||||
},
|
},
|
||||||
@@ -209,7 +208,7 @@ private fun DetailsHeader(
|
|||||||
extension: Extension,
|
extension: Extension,
|
||||||
onClickAgeRating: () -> Unit,
|
onClickAgeRating: () -> Unit,
|
||||||
onClickUninstall: () -> Unit,
|
onClickUninstall: () -> Unit,
|
||||||
onClickAppInfo: () -> Unit,
|
onClickAppInfo: (() -> Unit)?,
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
@@ -293,6 +292,7 @@ private fun DetailsHeader(
|
|||||||
top = MaterialTheme.padding.small,
|
top = MaterialTheme.padding.small,
|
||||||
bottom = MaterialTheme.padding.medium,
|
bottom = MaterialTheme.padding.medium,
|
||||||
),
|
),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
) {
|
) {
|
||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
@@ -301,20 +301,20 @@ private fun DetailsHeader(
|
|||||||
Text(stringResource(R.string.ext_uninstall))
|
Text(stringResource(R.string.ext_uninstall))
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(Modifier.width(16.dp))
|
if (onClickAppInfo != null) {
|
||||||
|
Button(
|
||||||
Button(
|
modifier = Modifier.weight(1f),
|
||||||
modifier = Modifier.weight(1f),
|
onClick = onClickAppInfo,
|
||||||
onClick = onClickAppInfo,
|
) {
|
||||||
) {
|
Text(
|
||||||
Text(
|
text = stringResource(R.string.ext_app_info),
|
||||||
text = stringResource(R.string.ext_app_info),
|
color = MaterialTheme.colorScheme.onPrimary,
|
||||||
color = MaterialTheme.colorScheme.onPrimary,
|
)
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Divider()
|
HorizontalDivider()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -356,11 +356,8 @@ private fun InfoText(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun InfoDivider() {
|
private fun InfoDivider() {
|
||||||
Divider(
|
VerticalDivider(
|
||||||
modifier = Modifier
|
modifier = Modifier.height(20.dp),
|
||||||
.height(20.dp)
|
|
||||||
.width(1.dp),
|
|
||||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = DIVIDER_ALPHA),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -415,7 +412,7 @@ private fun NsfwWarningDialog(
|
|||||||
},
|
},
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
TextButton(onClick = onClickConfirm) {
|
TextButton(onClick = onClickConfirm) {
|
||||||
Text(text = stringResource(android.R.string.ok))
|
Text(text = stringResource(R.string.action_ok))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onDismissRequest = onClickConfirm,
|
onDismissRequest = onClickConfirm,
|
||||||
|
|||||||
@@ -2,19 +2,19 @@ package eu.kanade.presentation.browse
|
|||||||
|
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import eu.kanade.presentation.components.AppBar
|
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.presentation.more.settings.widget.SwitchPreferenceWidget
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionFilterState
|
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionFilterState
|
||||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||||
|
import tachiyomi.presentation.core.components.material.Scaffold
|
||||||
|
import tachiyomi.presentation.core.screens.EmptyScreen
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ExtensionFilterScreen(
|
fun ExtensionFilterScreen(
|
||||||
@@ -53,7 +53,7 @@ private fun ExtensionFilterContent(
|
|||||||
onClickLang: (String) -> Unit,
|
onClickLang: (String) -> Unit,
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
FastScrollLazyColumn(
|
LazyColumn(
|
||||||
contentPadding = contentPadding,
|
contentPadding = contentPadding,
|
||||||
) {
|
) {
|
||||||
items(state.languages) { language ->
|
items(state.languages) { language ->
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ package eu.kanade.presentation.browse
|
|||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.compose.animation.core.animateDpAsState
|
import androidx.compose.animation.core.animateDpAsState
|
||||||
import androidx.compose.foundation.combinedClickable
|
import androidx.compose.foundation.combinedClickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.FlowRow
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.RowScope
|
import androidx.compose.foundation.layout.RowScope
|
||||||
@@ -34,29 +36,28 @@ import androidx.compose.ui.platform.LocalContext
|
|||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
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.BaseBrowseItem
|
||||||
import eu.kanade.presentation.browse.components.ExtensionIcon
|
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.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.R
|
||||||
import eu.kanade.tachiyomi.extension.model.Extension
|
import eu.kanade.tachiyomi.extension.model.Extension
|
||||||
import eu.kanade.tachiyomi.extension.model.InstallStep
|
import eu.kanade.tachiyomi.extension.model.InstallStep
|
||||||
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionUiModel
|
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionUiModel
|
||||||
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionsState
|
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionsScreenModel
|
||||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||||
|
import tachiyomi.presentation.core.components.FastScrollLazyColumn
|
||||||
|
import tachiyomi.presentation.core.components.material.PullRefresh
|
||||||
|
import tachiyomi.presentation.core.components.material.padding
|
||||||
|
import tachiyomi.presentation.core.components.material.topSmallPaddingValues
|
||||||
|
import tachiyomi.presentation.core.screens.EmptyScreen
|
||||||
|
import tachiyomi.presentation.core.screens.LoadingScreen
|
||||||
|
import tachiyomi.presentation.core.theme.header
|
||||||
|
import tachiyomi.presentation.core.util.plus
|
||||||
|
import tachiyomi.presentation.core.util.secondaryItemAlpha
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ExtensionScreen(
|
fun ExtensionScreen(
|
||||||
state: ExtensionsState,
|
state: ExtensionsScreenModel.State,
|
||||||
contentPadding: PaddingValues,
|
contentPadding: PaddingValues,
|
||||||
searchQuery: String?,
|
searchQuery: String?,
|
||||||
onLongClickItem: (Extension) -> Unit,
|
onLongClickItem: (Extension) -> Unit,
|
||||||
@@ -75,7 +76,7 @@ fun ExtensionScreen(
|
|||||||
enabled = !state.isLoading,
|
enabled = !state.isLoading,
|
||||||
) {
|
) {
|
||||||
when {
|
when {
|
||||||
state.isLoading -> LoadingScreen(modifier = Modifier.padding(contentPadding))
|
state.isLoading -> LoadingScreen(Modifier.padding(contentPadding))
|
||||||
state.isEmpty -> {
|
state.isEmpty -> {
|
||||||
val msg = if (!searchQuery.isNullOrEmpty()) {
|
val msg = if (!searchQuery.isNullOrEmpty()) {
|
||||||
R.string.no_results_found
|
R.string.no_results_found
|
||||||
@@ -107,7 +108,7 @@ fun ExtensionScreen(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ExtensionContent(
|
private fun ExtensionContent(
|
||||||
state: ExtensionsState,
|
state: ExtensionsScreenModel.State,
|
||||||
contentPadding: PaddingValues,
|
contentPadding: PaddingValues,
|
||||||
onLongClickItem: (Extension) -> Unit,
|
onLongClickItem: (Extension) -> Unit,
|
||||||
onClickItemCancel: (Extension) -> Unit,
|
onClickItemCancel: (Extension) -> Unit,
|
||||||
@@ -243,7 +244,10 @@ private fun ExtensionItem(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val padding by animateDpAsState(targetValue = if (idle) 0.dp else 8.dp)
|
val padding by animateDpAsState(
|
||||||
|
targetValue = if (idle) 0.dp else 8.dp,
|
||||||
|
label = "iconPadding",
|
||||||
|
)
|
||||||
ExtensionIcon(
|
ExtensionIcon(
|
||||||
extension = extension,
|
extension = extension,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -287,7 +291,7 @@ private fun ExtensionItemContent(
|
|||||||
// Won't look good but it's not like we can ellipsize overflowing content
|
// Won't look good but it's not like we can ellipsize overflowing content
|
||||||
FlowRow(
|
FlowRow(
|
||||||
modifier = Modifier.secondaryItemAlpha(),
|
modifier = Modifier.secondaryItemAlpha(),
|
||||||
mainAxisSpacing = 4.dp,
|
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
) {
|
) {
|
||||||
ProvideTextStyle(value = MaterialTheme.typography.bodySmall) {
|
ProvideTextStyle(value = MaterialTheme.typography.bodySmall) {
|
||||||
if (extension is Extension.Installed && extension.lang.isNotEmpty()) {
|
if (extension is Extension.Installed && extension.lang.isNotEmpty()) {
|
||||||
|
|||||||
@@ -1,35 +1,31 @@
|
|||||||
package eu.kanade.presentation.browse
|
package eu.kanade.presentation.browse
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.State
|
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.GlobalSearchCardRow
|
||||||
import eu.kanade.presentation.browse.components.GlobalSearchErrorResultItem
|
import eu.kanade.presentation.browse.components.GlobalSearchErrorResultItem
|
||||||
import eu.kanade.presentation.browse.components.GlobalSearchLoadingResultItem
|
import eu.kanade.presentation.browse.components.GlobalSearchLoadingResultItem
|
||||||
import eu.kanade.presentation.browse.components.GlobalSearchResultItem
|
import eu.kanade.presentation.browse.components.GlobalSearchResultItem
|
||||||
import eu.kanade.presentation.browse.components.GlobalSearchToolbar
|
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.source.CatalogueSource
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchState
|
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchItemResult
|
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchItemResult
|
||||||
|
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchScreenModel
|
||||||
|
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SourceFilter
|
||||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
|
import tachiyomi.presentation.core.components.material.Scaffold
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun GlobalSearchScreen(
|
fun GlobalSearchScreen(
|
||||||
state: GlobalSearchState,
|
state: SearchScreenModel.State,
|
||||||
navigateUp: () -> Unit,
|
navigateUp: () -> Unit,
|
||||||
onChangeSearchQuery: (String?) -> Unit,
|
onChangeSearchQuery: (String?) -> Unit,
|
||||||
onSearch: (String) -> Unit,
|
onSearch: (String) -> Unit,
|
||||||
getManga: @Composable (CatalogueSource, Manga) -> State<Manga>,
|
onChangeSearchFilter: (SourceFilter) -> Unit,
|
||||||
|
onToggleResults: () -> Unit,
|
||||||
|
getManga: @Composable (Manga) -> State<Manga>,
|
||||||
onClickSource: (CatalogueSource) -> Unit,
|
onClickSource: (CatalogueSource) -> Unit,
|
||||||
onClickItem: (Manga) -> Unit,
|
onClickItem: (Manga) -> Unit,
|
||||||
onLongClickItem: (Manga) -> Unit,
|
onLongClickItem: (Manga) -> Unit,
|
||||||
@@ -43,12 +39,16 @@ fun GlobalSearchScreen(
|
|||||||
navigateUp = navigateUp,
|
navigateUp = navigateUp,
|
||||||
onChangeSearchQuery = onChangeSearchQuery,
|
onChangeSearchQuery = onChangeSearchQuery,
|
||||||
onSearch = onSearch,
|
onSearch = onSearch,
|
||||||
|
sourceFilter = state.sourceFilter,
|
||||||
|
onChangeSearchFilter = onChangeSearchFilter,
|
||||||
|
onlyShowHasResults = state.onlyShowHasResults,
|
||||||
|
onToggleResults = onToggleResults,
|
||||||
scrollBehavior = scrollBehavior,
|
scrollBehavior = scrollBehavior,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
GlobalSearchContent(
|
GlobalSearchContent(
|
||||||
items = state.items,
|
items = state.filteredItems,
|
||||||
contentPadding = paddingValues,
|
contentPadding = paddingValues,
|
||||||
getManga = getManga,
|
getManga = getManga,
|
||||||
onClickSource = onClickSource,
|
onClickSource = onClickSource,
|
||||||
@@ -59,10 +59,11 @@ fun GlobalSearchScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun GlobalSearchContent(
|
internal fun GlobalSearchContent(
|
||||||
|
fromSourceId: Long? = null,
|
||||||
items: Map<CatalogueSource, SearchItemResult>,
|
items: Map<CatalogueSource, SearchItemResult>,
|
||||||
contentPadding: PaddingValues,
|
contentPadding: PaddingValues,
|
||||||
getManga: @Composable (CatalogueSource, Manga) -> State<Manga>,
|
getManga: @Composable (Manga) -> State<Manga>,
|
||||||
onClickSource: (CatalogueSource) -> Unit,
|
onClickSource: (CatalogueSource) -> Unit,
|
||||||
onClickItem: (Manga) -> Unit,
|
onClickItem: (Manga) -> Unit,
|
||||||
onLongClickItem: (Manga) -> Unit,
|
onLongClickItem: (Manga) -> Unit,
|
||||||
@@ -73,7 +74,7 @@ fun GlobalSearchContent(
|
|||||||
items.forEach { (source, result) ->
|
items.forEach { (source, result) ->
|
||||||
item(key = source.id) {
|
item(key = source.id) {
|
||||||
GlobalSearchResultItem(
|
GlobalSearchResultItem(
|
||||||
title = source.name,
|
title = fromSourceId?.let { "▶ ${source.name}".takeIf { source.id == fromSourceId } } ?: source.name,
|
||||||
subtitle = LocaleHelper.getDisplayName(source.lang),
|
subtitle = LocaleHelper.getDisplayName(source.lang),
|
||||||
onClick = { onClickSource(source) },
|
onClick = { onClickSource(source) },
|
||||||
) {
|
) {
|
||||||
@@ -82,21 +83,9 @@ fun GlobalSearchContent(
|
|||||||
GlobalSearchLoadingResultItem()
|
GlobalSearchLoadingResultItem()
|
||||||
}
|
}
|
||||||
is SearchItemResult.Success -> {
|
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(
|
GlobalSearchCardRow(
|
||||||
titles = result.result,
|
titles = result.result,
|
||||||
getManga = { getManga(source, it) },
|
getManga = getManga,
|
||||||
onClick = onClickItem,
|
onClick = onClickItem,
|
||||||
onLongClick = onLongClickItem,
|
onLongClick = onLongClickItem,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,19 +6,19 @@ import androidx.compose.foundation.lazy.items
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import eu.kanade.presentation.components.AppBar
|
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.presentation.manga.components.BaseMangaListItem
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.ui.browse.migration.manga.MigrateMangaState
|
import eu.kanade.tachiyomi.ui.browse.migration.manga.MigrateMangaScreenModel
|
||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
|
import tachiyomi.presentation.core.components.FastScrollLazyColumn
|
||||||
|
import tachiyomi.presentation.core.components.material.Scaffold
|
||||||
|
import tachiyomi.presentation.core.screens.EmptyScreen
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MigrateMangaScreen(
|
fun MigrateMangaScreen(
|
||||||
navigateUp: () -> Unit,
|
navigateUp: () -> Unit,
|
||||||
title: String?,
|
title: String?,
|
||||||
state: MigrateMangaState,
|
state: MigrateMangaScreenModel.State,
|
||||||
onClickItem: (Manga) -> Unit,
|
onClickItem: (Manga) -> Unit,
|
||||||
onClickCover: (Manga) -> Unit,
|
onClickCover: (Manga) -> Unit,
|
||||||
) {
|
) {
|
||||||
@@ -51,7 +51,7 @@ fun MigrateMangaScreen(
|
|||||||
@Composable
|
@Composable
|
||||||
private fun MigrateMangaContent(
|
private fun MigrateMangaContent(
|
||||||
contentPadding: PaddingValues,
|
contentPadding: PaddingValues,
|
||||||
state: MigrateMangaState,
|
state: MigrateMangaScreenModel.State,
|
||||||
onClickItem: (Manga) -> Unit,
|
onClickItem: (Manga) -> Unit,
|
||||||
onClickCover: (Manga) -> Unit,
|
onClickCover: (Manga) -> Unit,
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -1,29 +1,24 @@
|
|||||||
package eu.kanade.presentation.browse
|
package eu.kanade.presentation.browse
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.State
|
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.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.source.CatalogueSource
|
||||||
import eu.kanade.tachiyomi.ui.browse.migration.search.MigrateSearchState
|
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchScreenModel
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchItemResult
|
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SourceFilter
|
||||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
|
||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
|
import tachiyomi.presentation.core.components.material.Scaffold
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MigrateSearchScreen(
|
fun MigrateSearchScreen(
|
||||||
|
state: SearchScreenModel.State,
|
||||||
|
fromSourceId: Long?,
|
||||||
navigateUp: () -> Unit,
|
navigateUp: () -> Unit,
|
||||||
state: MigrateSearchState,
|
|
||||||
getManga: @Composable (CatalogueSource, Manga) -> State<Manga>,
|
|
||||||
onChangeSearchQuery: (String?) -> Unit,
|
onChangeSearchQuery: (String?) -> Unit,
|
||||||
onSearch: (String) -> Unit,
|
onSearch: (String) -> Unit,
|
||||||
|
onChangeSearchFilter: (SourceFilter) -> Unit,
|
||||||
|
onToggleResults: () -> Unit,
|
||||||
|
getManga: @Composable (Manga) -> State<Manga>,
|
||||||
onClickSource: (CatalogueSource) -> Unit,
|
onClickSource: (CatalogueSource) -> Unit,
|
||||||
onClickItem: (Manga) -> Unit,
|
onClickItem: (Manga) -> Unit,
|
||||||
onLongClickItem: (Manga) -> Unit,
|
onLongClickItem: (Manga) -> Unit,
|
||||||
@@ -37,13 +32,17 @@ fun MigrateSearchScreen(
|
|||||||
navigateUp = navigateUp,
|
navigateUp = navigateUp,
|
||||||
onChangeSearchQuery = onChangeSearchQuery,
|
onChangeSearchQuery = onChangeSearchQuery,
|
||||||
onSearch = onSearch,
|
onSearch = onSearch,
|
||||||
|
sourceFilter = state.sourceFilter,
|
||||||
|
onChangeSearchFilter = onChangeSearchFilter,
|
||||||
|
onlyShowHasResults = state.onlyShowHasResults,
|
||||||
|
onToggleResults = onToggleResults,
|
||||||
scrollBehavior = scrollBehavior,
|
scrollBehavior = scrollBehavior,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
MigrateSearchContent(
|
GlobalSearchContent(
|
||||||
sourceId = state.manga?.source ?: -1,
|
fromSourceId = fromSourceId,
|
||||||
items = state.items,
|
items = state.filteredItems,
|
||||||
contentPadding = paddingValues,
|
contentPadding = paddingValues,
|
||||||
getManga = getManga,
|
getManga = getManga,
|
||||||
onClickSource = onClickSource,
|
onClickSource = onClickSource,
|
||||||
@@ -52,50 +51,3 @@ fun MigrateSearchScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -25,25 +25,25 @@ import androidx.compose.ui.text.style.TextOverflow
|
|||||||
import eu.kanade.domain.source.interactor.SetMigrateSorting
|
import eu.kanade.domain.source.interactor.SetMigrateSorting
|
||||||
import eu.kanade.presentation.browse.components.BaseSourceItem
|
import eu.kanade.presentation.browse.components.BaseSourceItem
|
||||||
import eu.kanade.presentation.browse.components.SourceIcon
|
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.R
|
||||||
import eu.kanade.tachiyomi.ui.browse.migration.sources.MigrateSourceState
|
import eu.kanade.tachiyomi.ui.browse.migration.sources.MigrateSourceScreenModel
|
||||||
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
||||||
import tachiyomi.domain.source.model.Source
|
import tachiyomi.domain.source.model.Source
|
||||||
|
import tachiyomi.presentation.core.components.Badge
|
||||||
|
import tachiyomi.presentation.core.components.BadgeGroup
|
||||||
|
import tachiyomi.presentation.core.components.ScrollbarLazyColumn
|
||||||
|
import tachiyomi.presentation.core.components.Scroller.STICKY_HEADER_KEY_PREFIX
|
||||||
|
import tachiyomi.presentation.core.components.material.padding
|
||||||
|
import tachiyomi.presentation.core.components.material.topSmallPaddingValues
|
||||||
|
import tachiyomi.presentation.core.screens.EmptyScreen
|
||||||
|
import tachiyomi.presentation.core.screens.LoadingScreen
|
||||||
|
import tachiyomi.presentation.core.theme.header
|
||||||
|
import tachiyomi.presentation.core.util.plus
|
||||||
|
import tachiyomi.presentation.core.util.secondaryItemAlpha
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MigrateSourceScreen(
|
fun MigrateSourceScreen(
|
||||||
state: MigrateSourceState,
|
state: MigrateSourceScreenModel.State,
|
||||||
contentPadding: PaddingValues,
|
contentPadding: PaddingValues,
|
||||||
onClickItem: (Source) -> Unit,
|
onClickItem: (Source) -> Unit,
|
||||||
onToggleSortingDirection: () -> Unit,
|
onToggleSortingDirection: () -> Unit,
|
||||||
@@ -51,7 +51,7 @@ fun MigrateSourceScreen(
|
|||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
when {
|
when {
|
||||||
state.isLoading -> LoadingScreen(modifier = Modifier.padding(contentPadding))
|
state.isLoading -> LoadingScreen(Modifier.padding(contentPadding))
|
||||||
state.isEmpty -> EmptyScreen(
|
state.isEmpty -> EmptyScreen(
|
||||||
textResource = R.string.information_empty_library,
|
textResource = R.string.information_empty_library,
|
||||||
modifier = Modifier.padding(contentPadding),
|
modifier = Modifier.padding(contentPadding),
|
||||||
|
|||||||
@@ -10,19 +10,19 @@ import androidx.compose.ui.platform.LocalContext
|
|||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import eu.kanade.presentation.browse.components.BaseSourceItem
|
import eu.kanade.presentation.browse.components.BaseSourceItem
|
||||||
import eu.kanade.presentation.components.AppBar
|
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.presentation.more.settings.widget.SwitchPreferenceWidget
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.SourcesFilterState
|
import eu.kanade.tachiyomi.ui.browse.source.SourcesFilterScreenModel
|
||||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||||
import tachiyomi.domain.source.model.Source
|
import tachiyomi.domain.source.model.Source
|
||||||
|
import tachiyomi.presentation.core.components.FastScrollLazyColumn
|
||||||
|
import tachiyomi.presentation.core.components.material.Scaffold
|
||||||
|
import tachiyomi.presentation.core.screens.EmptyScreen
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SourcesFilterScreen(
|
fun SourcesFilterScreen(
|
||||||
navigateUp: () -> Unit,
|
navigateUp: () -> Unit,
|
||||||
state: SourcesFilterState.Success,
|
state: SourcesFilterScreenModel.State.Success,
|
||||||
onClickLanguage: (String) -> Unit,
|
onClickLanguage: (String) -> Unit,
|
||||||
onClickSource: (Source) -> Unit,
|
onClickSource: (Source) -> Unit,
|
||||||
) {
|
) {
|
||||||
@@ -54,7 +54,7 @@ fun SourcesFilterScreen(
|
|||||||
@Composable
|
@Composable
|
||||||
private fun SourcesFilterContent(
|
private fun SourcesFilterContent(
|
||||||
contentPadding: PaddingValues,
|
contentPadding: PaddingValues,
|
||||||
state: SourcesFilterState.Success,
|
state: SourcesFilterScreenModel.State.Success,
|
||||||
onClickLanguage: (String) -> Unit,
|
onClickLanguage: (String) -> Unit,
|
||||||
onClickSource: (Source) -> Unit,
|
onClickSource: (Source) -> Unit,
|
||||||
) {
|
) {
|
||||||
@@ -64,7 +64,7 @@ private fun SourcesFilterContent(
|
|||||||
state.items.forEach { (language, sources) ->
|
state.items.forEach { (language, sources) ->
|
||||||
val enabled = language in state.enabledLanguages
|
val enabled = language in state.enabledLanguages
|
||||||
item(
|
item(
|
||||||
key = language.hashCode(),
|
key = language,
|
||||||
contentType = "source-filter-header",
|
contentType = "source-filter-header",
|
||||||
) {
|
) {
|
||||||
SourcesFilterHeader(
|
SourcesFilterHeader(
|
||||||
@@ -74,18 +74,19 @@ private fun SourcesFilterContent(
|
|||||||
onClickItem = onClickLanguage,
|
onClickItem = onClickLanguage,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (!enabled) return@forEach
|
if (enabled) {
|
||||||
items(
|
items(
|
||||||
items = sources,
|
items = sources,
|
||||||
key = { "source-filter-${it.key()}" },
|
key = { "source-filter-${it.key()}" },
|
||||||
contentType = { "source-filter-item" },
|
contentType = { "source-filter-item" },
|
||||||
) { source ->
|
) { source ->
|
||||||
SourcesFilterItem(
|
SourcesFilterItem(
|
||||||
modifier = Modifier.animateItemPlacement(),
|
modifier = Modifier.animateItemPlacement(),
|
||||||
source = source,
|
source = source,
|
||||||
enabled = "${source.id}" !in state.disabledSources,
|
enabled = "${source.id}" !in state.disabledSources,
|
||||||
onClickItem = onClickSource,
|
onClickItem = onClickSource,
|
||||||
)
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,31 +22,32 @@ import androidx.compose.ui.platform.LocalContext
|
|||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import eu.kanade.presentation.browse.components.BaseSourceItem
|
import eu.kanade.presentation.browse.components.BaseSourceItem
|
||||||
import eu.kanade.presentation.components.EmptyScreen
|
|
||||||
import eu.kanade.presentation.components.LoadingScreen
|
|
||||||
import eu.kanade.presentation.components.ScrollbarLazyColumn
|
|
||||||
import eu.kanade.presentation.theme.header
|
|
||||||
import eu.kanade.presentation.util.padding
|
|
||||||
import eu.kanade.presentation.util.plus
|
|
||||||
import eu.kanade.presentation.util.topSmallPaddingValues
|
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.source.LocalSource
|
import eu.kanade.tachiyomi.ui.browse.source.SourcesScreenModel
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.SourcesState
|
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreenModel.Listing
|
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreenModel.Listing
|
||||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||||
import tachiyomi.domain.source.model.Pin
|
import tachiyomi.domain.source.model.Pin
|
||||||
import tachiyomi.domain.source.model.Source
|
import tachiyomi.domain.source.model.Source
|
||||||
|
import tachiyomi.presentation.core.components.ScrollbarLazyColumn
|
||||||
|
import tachiyomi.presentation.core.components.material.SecondaryItemAlpha
|
||||||
|
import tachiyomi.presentation.core.components.material.padding
|
||||||
|
import tachiyomi.presentation.core.components.material.topSmallPaddingValues
|
||||||
|
import tachiyomi.presentation.core.screens.EmptyScreen
|
||||||
|
import tachiyomi.presentation.core.screens.LoadingScreen
|
||||||
|
import tachiyomi.presentation.core.theme.header
|
||||||
|
import tachiyomi.presentation.core.util.plus
|
||||||
|
import tachiyomi.source.local.isLocal
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SourcesScreen(
|
fun SourcesScreen(
|
||||||
state: SourcesState,
|
state: SourcesScreenModel.State,
|
||||||
contentPadding: PaddingValues,
|
contentPadding: PaddingValues,
|
||||||
onClickItem: (Source, Listing) -> Unit,
|
onClickItem: (Source, Listing) -> Unit,
|
||||||
onClickPin: (Source) -> Unit,
|
onClickPin: (Source) -> Unit,
|
||||||
onLongClickItem: (Source) -> Unit,
|
onLongClickItem: (Source) -> Unit,
|
||||||
) {
|
) {
|
||||||
when {
|
when {
|
||||||
state.isLoading -> LoadingScreen(modifier = Modifier.padding(contentPadding))
|
state.isLoading -> LoadingScreen(Modifier.padding(contentPadding))
|
||||||
state.isEmpty -> EmptyScreen(
|
state.isEmpty -> EmptyScreen(
|
||||||
textResource = R.string.source_empty_screen,
|
textResource = R.string.source_empty_screen,
|
||||||
modifier = Modifier.padding(contentPadding),
|
modifier = Modifier.padding(contentPadding),
|
||||||
@@ -143,7 +144,7 @@ private fun SourcePinButton(
|
|||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
) {
|
) {
|
||||||
val icon = if (isPinned) Icons.Filled.PushPin else Icons.Outlined.PushPin
|
val icon = if (isPinned) Icons.Filled.PushPin else Icons.Outlined.PushPin
|
||||||
val tint = if (isPinned) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onBackground
|
val tint = if (isPinned) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onBackground.copy(alpha = SecondaryItemAlpha)
|
||||||
val description = if (isPinned) R.string.action_unpin else R.string.action_pin
|
val description = if (isPinned) R.string.action_unpin else R.string.action_pin
|
||||||
IconButton(onClick = onClick) {
|
IconButton(onClick = onClick) {
|
||||||
Icon(
|
Icon(
|
||||||
@@ -175,7 +176,7 @@ fun SourceOptionsDialog(
|
|||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(vertical = 16.dp),
|
.padding(vertical = 16.dp),
|
||||||
)
|
)
|
||||||
if (source.id != LocalSource.ID) {
|
if (!source.isLocal()) {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.action_disable),
|
text = stringResource(R.string.action_disable),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -191,7 +192,7 @@ fun SourceOptionsDialog(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed class SourceUiModel {
|
sealed interface SourceUiModel {
|
||||||
data class Item(val source: Source) : SourceUiModel()
|
data class Item(val source: Source) : SourceUiModel
|
||||||
data class Header(val language: String) : SourceUiModel()
|
data class Header(val language: String) : SourceUiModel
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import androidx.compose.material3.MaterialTheme
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import eu.kanade.presentation.util.padding
|
import tachiyomi.presentation.core.components.material.padding
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun BaseBrowseItem(
|
fun BaseBrowseItem(
|
||||||
|
|||||||
@@ -9,10 +9,10 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import eu.kanade.presentation.util.padding
|
|
||||||
import eu.kanade.presentation.util.secondaryItemAlpha
|
|
||||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||||
import tachiyomi.domain.source.model.Source
|
import tachiyomi.domain.source.model.Source
|
||||||
|
import tachiyomi.presentation.core.components.material.padding
|
||||||
|
import tachiyomi.presentation.core.util.secondaryItemAlpha
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun BaseSourceItem(
|
fun BaseSourceItem(
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package eu.kanade.presentation.browse.components
|
||||||
|
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.CollectionsBookmark
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import tachiyomi.presentation.core.components.Badge
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun InLibraryBadge(enabled: Boolean) {
|
||||||
|
if (enabled) {
|
||||||
|
Badge(
|
||||||
|
imageVector = Icons.Outlined.CollectionsBookmark,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
package eu.kanade.presentation.browse.components
|
package eu.kanade.presentation.browse.components
|
||||||
|
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import android.util.DisplayMetrics
|
import android.util.DisplayMetrics
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
@@ -31,9 +30,10 @@ import eu.kanade.domain.source.model.icon
|
|||||||
import eu.kanade.presentation.util.rememberResourceBitmapPainter
|
import eu.kanade.presentation.util.rememberResourceBitmapPainter
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.extension.model.Extension
|
import eu.kanade.tachiyomi.extension.model.Extension
|
||||||
import eu.kanade.tachiyomi.source.LocalSource
|
import eu.kanade.tachiyomi.extension.util.ExtensionLoader
|
||||||
import tachiyomi.core.util.lang.withIOContext
|
import tachiyomi.core.util.lang.withIOContext
|
||||||
import tachiyomi.domain.source.model.Source
|
import tachiyomi.domain.source.model.Source
|
||||||
|
import tachiyomi.source.local.isLocal
|
||||||
|
|
||||||
private val defaultModifier = Modifier
|
private val defaultModifier = Modifier
|
||||||
.height(40.dp)
|
.height(40.dp)
|
||||||
@@ -62,7 +62,7 @@ fun SourceIcon(
|
|||||||
modifier = modifier.then(defaultModifier),
|
modifier = modifier.then(defaultModifier),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
source.id == LocalSource.ID -> {
|
source.isLocal() -> {
|
||||||
Image(
|
Image(
|
||||||
painter = painterResource(R.mipmap.ic_local_source),
|
painter = painterResource(R.mipmap.ic_local_source),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
@@ -127,7 +127,7 @@ private fun Extension.getIcon(density: Int = DisplayMetrics.DENSITY_DEFAULT): St
|
|||||||
return produceState<Result<ImageBitmap>>(initialValue = Result.Loading, this) {
|
return produceState<Result<ImageBitmap>>(initialValue = Result.Loading, this) {
|
||||||
withIOContext {
|
withIOContext {
|
||||||
value = try {
|
value = try {
|
||||||
val appInfo = context.packageManager.getApplicationInfo(pkgName, PackageManager.GET_META_DATA)
|
val appInfo = ExtensionLoader.getExtensionPackageInfoFromPkgName(context, pkgName)!!.applicationInfo
|
||||||
val appResources = context.packageManager.getResourcesForApplication(appInfo)
|
val appResources = context.packageManager.getResourcesForApplication(appInfo)
|
||||||
Result.Success(
|
Result.Success(
|
||||||
appResources.getDrawableForDensity(appInfo.icon, density, null)!!
|
appResources.getDrawableForDensity(appInfo.icon, density, null)!!
|
||||||
@@ -142,7 +142,7 @@ private fun Extension.getIcon(density: Int = DisplayMetrics.DENSITY_DEFAULT): St
|
|||||||
}
|
}
|
||||||
|
|
||||||
sealed class Result<out T> {
|
sealed class Result<out T> {
|
||||||
object Loading : Result<Nothing>()
|
data object Loading : Result<Nothing>()
|
||||||
object Error : Result<Nothing>()
|
data object Error : Result<Nothing>()
|
||||||
data class Success<out T>(val value: T) : Result<T>()
|
data class Success<out T>(val value: T) : Result<T>()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,13 +11,12 @@ import androidx.compose.runtime.getValue
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.paging.LoadState
|
import androidx.paging.LoadState
|
||||||
import androidx.paging.compose.LazyPagingItems
|
import androidx.paging.compose.LazyPagingItems
|
||||||
import eu.kanade.presentation.browse.InLibraryBadge
|
import eu.kanade.presentation.library.components.CommonMangaItemDefaults
|
||||||
import eu.kanade.presentation.components.CommonMangaItemDefaults
|
import eu.kanade.presentation.library.components.MangaComfortableGridItem
|
||||||
import eu.kanade.presentation.components.MangaComfortableGridItem
|
|
||||||
import eu.kanade.presentation.util.plus
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
import tachiyomi.domain.manga.model.MangaCover
|
import tachiyomi.domain.manga.model.MangaCover
|
||||||
|
import tachiyomi.presentation.core.util.plus
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun BrowseSourceComfortableGrid(
|
fun BrowseSourceComfortableGrid(
|
||||||
@@ -39,7 +38,7 @@ fun BrowseSourceComfortableGrid(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
items(mangaList.itemCount) { index ->
|
items(count = mangaList.itemCount) { index ->
|
||||||
val manga by mangaList[index]?.collectAsState() ?: return@items
|
val manga by mangaList[index]?.collectAsState() ?: return@items
|
||||||
BrowseSourceComfortableGridItem(
|
BrowseSourceComfortableGridItem(
|
||||||
manga = manga,
|
manga = manga,
|
||||||
@@ -57,7 +56,7 @@ fun BrowseSourceComfortableGrid(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun BrowseSourceComfortableGridItem(
|
private fun BrowseSourceComfortableGridItem(
|
||||||
manga: Manga,
|
manga: Manga,
|
||||||
onClick: () -> Unit = {},
|
onClick: () -> Unit = {},
|
||||||
onLongClick: () -> Unit = onClick,
|
onLongClick: () -> Unit = onClick,
|
||||||
|
|||||||
@@ -11,13 +11,12 @@ import androidx.compose.runtime.getValue
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.paging.LoadState
|
import androidx.paging.LoadState
|
||||||
import androidx.paging.compose.LazyPagingItems
|
import androidx.paging.compose.LazyPagingItems
|
||||||
import eu.kanade.presentation.browse.InLibraryBadge
|
import eu.kanade.presentation.library.components.CommonMangaItemDefaults
|
||||||
import eu.kanade.presentation.components.CommonMangaItemDefaults
|
import eu.kanade.presentation.library.components.MangaCompactGridItem
|
||||||
import eu.kanade.presentation.components.MangaCompactGridItem
|
|
||||||
import eu.kanade.presentation.util.plus
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
import tachiyomi.domain.manga.model.MangaCover
|
import tachiyomi.domain.manga.model.MangaCover
|
||||||
|
import tachiyomi.presentation.core.util.plus
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun BrowseSourceCompactGrid(
|
fun BrowseSourceCompactGrid(
|
||||||
@@ -39,7 +38,7 @@ fun BrowseSourceCompactGrid(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
items(mangaList.itemCount) { index ->
|
items(count = mangaList.itemCount) { index ->
|
||||||
val manga by mangaList[index]?.collectAsState() ?: return@items
|
val manga by mangaList[index]?.collectAsState() ?: return@items
|
||||||
BrowseSourceCompactGridItem(
|
BrowseSourceCompactGridItem(
|
||||||
manga = manga,
|
manga = manga,
|
||||||
|
|||||||
@@ -1,21 +1,19 @@
|
|||||||
package eu.kanade.presentation.browse.components
|
package eu.kanade.presentation.browse.components
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.paging.LoadState
|
import androidx.paging.LoadState
|
||||||
import androidx.paging.compose.LazyPagingItems
|
import androidx.paging.compose.LazyPagingItems
|
||||||
import androidx.paging.compose.items
|
import eu.kanade.presentation.library.components.CommonMangaItemDefaults
|
||||||
import eu.kanade.presentation.browse.InLibraryBadge
|
import eu.kanade.presentation.library.components.MangaListItem
|
||||||
import eu.kanade.presentation.components.CommonMangaItemDefaults
|
|
||||||
import eu.kanade.presentation.components.LazyColumn
|
|
||||||
import eu.kanade.presentation.components.MangaListItem
|
|
||||||
import eu.kanade.presentation.util.plus
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
import tachiyomi.domain.manga.model.MangaCover
|
import tachiyomi.domain.manga.model.MangaCover
|
||||||
|
import tachiyomi.presentation.core.util.plus
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun BrowseSourceList(
|
fun BrowseSourceList(
|
||||||
@@ -33,9 +31,8 @@ fun BrowseSourceList(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
items(mangaList) { mangaflow ->
|
items(count = mangaList.itemCount) { index ->
|
||||||
mangaflow ?: return@items
|
val manga by mangaList[index]?.collectAsState() ?: return@items
|
||||||
val manga by mangaflow.collectAsState()
|
|
||||||
BrowseSourceListItem(
|
BrowseSourceListItem(
|
||||||
manga = manga,
|
manga = manga,
|
||||||
onClick = { onMangaClick(manga) },
|
onClick = { onMangaClick(manga) },
|
||||||
@@ -52,7 +49,7 @@ fun BrowseSourceList(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun BrowseSourceListItem(
|
private fun BrowseSourceListItem(
|
||||||
manga: Manga,
|
manga: Manga,
|
||||||
onClick: () -> Unit = {},
|
onClick: () -> Unit = {},
|
||||||
onLongClick: () -> Unit = onClick,
|
onLongClick: () -> Unit = onClick,
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun BrowseSourceLoadingItem() {
|
internal fun BrowseSourceLoadingItem() {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ package eu.kanade.presentation.browse.components
|
|||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.ViewList
|
import androidx.compose.material.icons.filled.ViewList
|
||||||
import androidx.compose.material.icons.filled.ViewModule
|
import androidx.compose.material.icons.filled.ViewModule
|
||||||
import androidx.compose.material.icons.outlined.Help
|
|
||||||
import androidx.compose.material.icons.outlined.Public
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -20,26 +18,31 @@ import eu.kanade.presentation.components.DropdownMenu
|
|||||||
import eu.kanade.presentation.components.RadioMenuItem
|
import eu.kanade.presentation.components.RadioMenuItem
|
||||||
import eu.kanade.presentation.components.SearchToolbar
|
import eu.kanade.presentation.components.SearchToolbar
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||||
import eu.kanade.tachiyomi.source.LocalSource
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import tachiyomi.domain.library.model.LibraryDisplayMode
|
import tachiyomi.domain.library.model.LibraryDisplayMode
|
||||||
|
import tachiyomi.source.local.LocalSource
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun BrowseSourceToolbar(
|
fun BrowseSourceToolbar(
|
||||||
searchQuery: String?,
|
searchQuery: String?,
|
||||||
onSearchQueryChange: (String?) -> Unit,
|
onSearchQueryChange: (String?) -> Unit,
|
||||||
source: CatalogueSource?,
|
source: Source?,
|
||||||
displayMode: LibraryDisplayMode,
|
displayMode: LibraryDisplayMode,
|
||||||
onDisplayModeChange: (LibraryDisplayMode) -> Unit,
|
onDisplayModeChange: (LibraryDisplayMode) -> Unit,
|
||||||
navigateUp: () -> Unit,
|
navigateUp: () -> Unit,
|
||||||
onWebViewClick: () -> Unit,
|
onWebViewClick: () -> Unit,
|
||||||
onHelpClick: () -> Unit,
|
onHelpClick: () -> Unit,
|
||||||
|
onSettingsClick: () -> Unit,
|
||||||
onSearch: (String) -> Unit,
|
onSearch: (String) -> Unit,
|
||||||
scrollBehavior: TopAppBarScrollBehavior? = null,
|
scrollBehavior: TopAppBarScrollBehavior? = null,
|
||||||
) {
|
) {
|
||||||
// Avoid capturing unstable source in actions lambda
|
// Avoid capturing unstable source in actions lambda
|
||||||
val title = source?.name
|
val title = source?.name
|
||||||
val isLocalSource = source is LocalSource
|
val isLocalSource = source is LocalSource
|
||||||
|
val isConfigurableSource = source is ConfigurableSource
|
||||||
|
|
||||||
|
var selectingDisplayMode by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
SearchToolbar(
|
SearchToolbar(
|
||||||
navigateUp = navigateUp,
|
navigateUp = navigateUp,
|
||||||
@@ -49,29 +52,31 @@ fun BrowseSourceToolbar(
|
|||||||
onSearch = onSearch,
|
onSearch = onSearch,
|
||||||
onClickCloseSearch = navigateUp,
|
onClickCloseSearch = navigateUp,
|
||||||
actions = {
|
actions = {
|
||||||
var selectingDisplayMode by remember { mutableStateOf(false) }
|
|
||||||
AppBarActions(
|
AppBarActions(
|
||||||
actions = listOf(
|
actions = listOfNotNull(
|
||||||
AppBar.Action(
|
AppBar.Action(
|
||||||
title = stringResource(R.string.action_display_mode),
|
title = stringResource(R.string.action_display_mode),
|
||||||
icon = if (displayMode == LibraryDisplayMode.List) Icons.Filled.ViewList else Icons.Filled.ViewModule,
|
icon = if (displayMode == LibraryDisplayMode.List) Icons.Filled.ViewList else Icons.Filled.ViewModule,
|
||||||
onClick = { selectingDisplayMode = true },
|
onClick = { selectingDisplayMode = true },
|
||||||
),
|
),
|
||||||
if (isLocalSource) {
|
if (isLocalSource) {
|
||||||
AppBar.Action(
|
AppBar.OverflowAction(
|
||||||
title = stringResource(R.string.label_help),
|
title = stringResource(R.string.label_help),
|
||||||
icon = Icons.Outlined.Help,
|
|
||||||
onClick = onHelpClick,
|
onClick = onHelpClick,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
AppBar.Action(
|
AppBar.OverflowAction(
|
||||||
title = stringResource(R.string.action_web_view),
|
title = stringResource(R.string.action_open_in_web_view),
|
||||||
icon = Icons.Outlined.Public,
|
|
||||||
onClick = onWebViewClick,
|
onClick = onWebViewClick,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
AppBar.OverflowAction(
|
||||||
|
title = stringResource(R.string.action_settings),
|
||||||
|
onClick = onSettingsClick,
|
||||||
|
).takeIf { isConfigurableSource },
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
DropdownMenu(
|
DropdownMenu(
|
||||||
expanded = selectingDisplayMode,
|
expanded = selectingDisplayMode,
|
||||||
onDismissRequest = { selectingDisplayMode = false },
|
onDismissRequest = { selectingDisplayMode = false },
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
package eu.kanade.presentation.browse.components
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import eu.kanade.presentation.browse.InLibraryBadge
|
|
||||||
import eu.kanade.presentation.components.CommonMangaItemDefaults
|
|
||||||
import eu.kanade.presentation.components.MangaComfortableGridItem
|
|
||||||
import tachiyomi.domain.manga.model.MangaCover
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun GlobalSearchCard(
|
|
||||||
title: String,
|
|
||||||
cover: MangaCover,
|
|
||||||
isFavorite: Boolean,
|
|
||||||
onClick: () -> Unit,
|
|
||||||
onLongClick: () -> Unit,
|
|
||||||
) {
|
|
||||||
Box(modifier = Modifier.width(96.dp)) {
|
|
||||||
MangaComfortableGridItem(
|
|
||||||
title = title,
|
|
||||||
coverData = cover,
|
|
||||||
coverBadgeStart = {
|
|
||||||
InLibraryBadge(enabled = isFavorite)
|
|
||||||
},
|
|
||||||
coverAlpha = if (isFavorite) CommonMangaItemDefaults.BrowseFavoriteCoverAlpha else 1f,
|
|
||||||
onClick = onClick,
|
|
||||||
onLongClick = onLongClick,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,16 +1,27 @@
|
|||||||
package eu.kanade.presentation.browse.components
|
package eu.kanade.presentation.browse.components
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.lazy.LazyRow
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.State
|
import androidx.compose.runtime.State
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import eu.kanade.presentation.util.padding
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import eu.kanade.presentation.library.components.CommonMangaItemDefaults
|
||||||
|
import eu.kanade.presentation.library.components.MangaComfortableGridItem
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
|
import tachiyomi.domain.manga.model.MangaCover
|
||||||
import tachiyomi.domain.manga.model.asMangaCover
|
import tachiyomi.domain.manga.model.asMangaCover
|
||||||
|
import tachiyomi.presentation.core.components.material.padding
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun GlobalSearchCardRow(
|
fun GlobalSearchCardRow(
|
||||||
@@ -19,13 +30,18 @@ fun GlobalSearchCardRow(
|
|||||||
onClick: (Manga) -> Unit,
|
onClick: (Manga) -> Unit,
|
||||||
onLongClick: (Manga) -> Unit,
|
onLongClick: (Manga) -> Unit,
|
||||||
) {
|
) {
|
||||||
|
if (titles.isEmpty()) {
|
||||||
|
EmptyResultItem()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
LazyRow(
|
LazyRow(
|
||||||
contentPadding = PaddingValues(MaterialTheme.padding.small),
|
contentPadding = PaddingValues(MaterialTheme.padding.small),
|
||||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.tiny),
|
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.tiny),
|
||||||
) {
|
) {
|
||||||
items(titles) {
|
items(titles) {
|
||||||
val title by getManga(it)
|
val title by getManga(it)
|
||||||
GlobalSearchCard(
|
MangaItem(
|
||||||
title = title.title,
|
title = title.title,
|
||||||
cover = title.asMangaCover(),
|
cover = title.asMangaCover(),
|
||||||
isFavorite = title.favorite,
|
isFavorite = title.favorite,
|
||||||
@@ -35,3 +51,38 @@ fun GlobalSearchCardRow(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun MangaItem(
|
||||||
|
title: String,
|
||||||
|
cover: MangaCover,
|
||||||
|
isFavorite: Boolean,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
onLongClick: () -> Unit,
|
||||||
|
) {
|
||||||
|
Box(modifier = Modifier.width(96.dp)) {
|
||||||
|
MangaComfortableGridItem(
|
||||||
|
title = title,
|
||||||
|
titleMaxLines = 3,
|
||||||
|
coverData = cover,
|
||||||
|
coverBadgeStart = {
|
||||||
|
InLibraryBadge(enabled = isFavorite)
|
||||||
|
},
|
||||||
|
coverAlpha = if (isFavorite) CommonMangaItemDefaults.BrowseFavoriteCoverAlpha else 1f,
|
||||||
|
onClick = onClick,
|
||||||
|
onLongClick = onLongClick,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun EmptyResultItem() {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.no_results_found),
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(
|
||||||
|
horizontal = MaterialTheme.padding.medium,
|
||||||
|
vertical = MaterialTheme.padding.small,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import eu.kanade.presentation.util.padding
|
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
|
import tachiyomi.presentation.core.components.material.padding
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun GlobalSearchResultItem(
|
fun GlobalSearchResultItem(
|
||||||
@@ -61,18 +61,6 @@ fun GlobalSearchResultItem(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun GlobalSearchEmptyResultItem() {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.no_results_found),
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(
|
|
||||||
horizontal = MaterialTheme.padding.medium,
|
|
||||||
vertical = MaterialTheme.padding.small,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun GlobalSearchLoadingResultItem() {
|
fun GlobalSearchLoadingResultItem() {
|
||||||
Box(
|
Box(
|
||||||
|
|||||||
@@ -1,13 +1,36 @@
|
|||||||
package eu.kanade.presentation.browse.components
|
package eu.kanade.presentation.browse.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.horizontalScroll
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.DoneAll
|
||||||
|
import androidx.compose.material.icons.outlined.FilterList
|
||||||
|
import androidx.compose.material.icons.outlined.PushPin
|
||||||
|
import androidx.compose.material3.FilterChip
|
||||||
|
import androidx.compose.material3.FilterChipDefaults
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.LinearProgressIndicator
|
import androidx.compose.material3.LinearProgressIndicator
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||||
|
import androidx.compose.material3.VerticalDivider
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import eu.kanade.presentation.components.SearchToolbar
|
import eu.kanade.presentation.components.SearchToolbar
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SourceFilter
|
||||||
|
import tachiyomi.presentation.core.components.material.padding
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun GlobalSearchToolbar(
|
fun GlobalSearchToolbar(
|
||||||
@@ -17,24 +40,89 @@ fun GlobalSearchToolbar(
|
|||||||
navigateUp: () -> Unit,
|
navigateUp: () -> Unit,
|
||||||
onChangeSearchQuery: (String?) -> Unit,
|
onChangeSearchQuery: (String?) -> Unit,
|
||||||
onSearch: (String) -> Unit,
|
onSearch: (String) -> Unit,
|
||||||
|
sourceFilter: SourceFilter,
|
||||||
|
onChangeSearchFilter: (SourceFilter) -> Unit,
|
||||||
|
onlyShowHasResults: Boolean,
|
||||||
|
onToggleResults: () -> Unit,
|
||||||
scrollBehavior: TopAppBarScrollBehavior,
|
scrollBehavior: TopAppBarScrollBehavior,
|
||||||
) {
|
) {
|
||||||
Box {
|
Column(modifier = Modifier.background(MaterialTheme.colorScheme.surface)) {
|
||||||
SearchToolbar(
|
Box {
|
||||||
searchQuery = searchQuery,
|
SearchToolbar(
|
||||||
onChangeSearchQuery = onChangeSearchQuery,
|
searchQuery = searchQuery,
|
||||||
onSearch = onSearch,
|
onChangeSearchQuery = onChangeSearchQuery,
|
||||||
onClickCloseSearch = navigateUp,
|
onSearch = onSearch,
|
||||||
navigateUp = navigateUp,
|
onClickCloseSearch = navigateUp,
|
||||||
scrollBehavior = scrollBehavior,
|
navigateUp = navigateUp,
|
||||||
)
|
scrollBehavior = scrollBehavior,
|
||||||
if (progress in 1 until total) {
|
)
|
||||||
LinearProgressIndicator(
|
if (progress in 1..<total) {
|
||||||
progress = progress / total.toFloat(),
|
LinearProgressIndicator(
|
||||||
modifier = Modifier
|
progress = progress / total.toFloat(),
|
||||||
.align(Alignment.BottomStart)
|
modifier = Modifier
|
||||||
.fillMaxWidth(),
|
.align(Alignment.BottomStart)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.horizontalScroll(rememberScrollState())
|
||||||
|
.padding(horizontal = MaterialTheme.padding.small),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
||||||
|
) {
|
||||||
|
// TODO: make this UX better; it only applies when triggering a new search
|
||||||
|
FilterChip(
|
||||||
|
selected = sourceFilter == SourceFilter.PinnedOnly,
|
||||||
|
onClick = { onChangeSearchFilter(SourceFilter.PinnedOnly) },
|
||||||
|
leadingIcon = {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.PushPin,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier
|
||||||
|
.size(FilterChipDefaults.IconSize),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
label = {
|
||||||
|
Text(text = stringResource(id = R.string.pinned_sources))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
FilterChip(
|
||||||
|
selected = sourceFilter == SourceFilter.All,
|
||||||
|
onClick = { onChangeSearchFilter(SourceFilter.All) },
|
||||||
|
leadingIcon = {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.DoneAll,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier
|
||||||
|
.size(FilterChipDefaults.IconSize),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
label = {
|
||||||
|
Text(text = stringResource(id = R.string.all))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
VerticalDivider()
|
||||||
|
|
||||||
|
FilterChip(
|
||||||
|
selected = onlyShowHasResults,
|
||||||
|
onClick = { onToggleResults() },
|
||||||
|
leadingIcon = {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.FilterList,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier
|
||||||
|
.size(FilterChipDefaults.IconSize),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
label = {
|
||||||
|
Text(text = stringResource(id = R.string.has_results))
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
HorizontalDivider()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,28 +1,36 @@
|
|||||||
package eu.kanade.presentation.category
|
package eu.kanade.presentation.category
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.SortByAlpha
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import eu.kanade.presentation.category.components.CategoryContent
|
|
||||||
import eu.kanade.presentation.category.components.CategoryFloatingActionButton
|
import eu.kanade.presentation.category.components.CategoryFloatingActionButton
|
||||||
|
import eu.kanade.presentation.category.components.CategoryListItem
|
||||||
import eu.kanade.presentation.components.AppBar
|
import eu.kanade.presentation.components.AppBar
|
||||||
import eu.kanade.presentation.components.EmptyScreen
|
import eu.kanade.presentation.components.AppBarActions
|
||||||
import eu.kanade.presentation.components.Scaffold
|
|
||||||
import eu.kanade.presentation.util.padding
|
|
||||||
import eu.kanade.presentation.util.plus
|
|
||||||
import eu.kanade.presentation.util.topSmallPaddingValues
|
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.ui.category.CategoryScreenState
|
import eu.kanade.tachiyomi.ui.category.CategoryScreenState
|
||||||
import tachiyomi.domain.category.model.Category
|
import tachiyomi.domain.category.model.Category
|
||||||
|
import tachiyomi.presentation.core.components.material.Scaffold
|
||||||
|
import tachiyomi.presentation.core.components.material.padding
|
||||||
|
import tachiyomi.presentation.core.components.material.topSmallPaddingValues
|
||||||
|
import tachiyomi.presentation.core.screens.EmptyScreen
|
||||||
|
import tachiyomi.presentation.core.util.plus
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun CategoryScreen(
|
fun CategoryScreen(
|
||||||
state: CategoryScreenState.Success,
|
state: CategoryScreenState.Success,
|
||||||
onClickCreate: () -> Unit,
|
onClickCreate: () -> Unit,
|
||||||
|
onClickSortAlphabetically: () -> Unit,
|
||||||
onClickRename: (Category) -> Unit,
|
onClickRename: (Category) -> Unit,
|
||||||
onClickDelete: (Category) -> Unit,
|
onClickDelete: (Category) -> Unit,
|
||||||
onClickMoveUp: (Category) -> Unit,
|
onClickMoveUp: (Category) -> Unit,
|
||||||
@@ -35,6 +43,17 @@ fun CategoryScreen(
|
|||||||
AppBar(
|
AppBar(
|
||||||
title = stringResource(R.string.action_edit_categories),
|
title = stringResource(R.string.action_edit_categories),
|
||||||
navigateUp = navigateUp,
|
navigateUp = navigateUp,
|
||||||
|
actions = {
|
||||||
|
AppBarActions(
|
||||||
|
listOf(
|
||||||
|
AppBar.Action(
|
||||||
|
title = stringResource(R.string.action_sort),
|
||||||
|
icon = Icons.Outlined.SortByAlpha,
|
||||||
|
onClick = onClickSortAlphabetically,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
},
|
||||||
scrollBehavior = scrollBehavior,
|
scrollBehavior = scrollBehavior,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -64,3 +83,36 @@ fun CategoryScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun CategoryContent(
|
||||||
|
categories: List<Category>,
|
||||||
|
lazyListState: LazyListState,
|
||||||
|
paddingValues: PaddingValues,
|
||||||
|
onClickRename: (Category) -> Unit,
|
||||||
|
onClickDelete: (Category) -> Unit,
|
||||||
|
onMoveUp: (Category) -> Unit,
|
||||||
|
onMoveDown: (Category) -> Unit,
|
||||||
|
) {
|
||||||
|
LazyColumn(
|
||||||
|
state = lazyListState,
|
||||||
|
contentPadding = paddingValues,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
||||||
|
) {
|
||||||
|
itemsIndexed(
|
||||||
|
items = categories,
|
||||||
|
key = { _, category -> "category-${category.id}" },
|
||||||
|
) { index, category ->
|
||||||
|
CategoryListItem(
|
||||||
|
modifier = Modifier.animateItemPlacement(),
|
||||||
|
category = category,
|
||||||
|
canMoveUp = index != 0,
|
||||||
|
canMoveDown = index != categories.lastIndex,
|
||||||
|
onMoveUp = onMoveUp,
|
||||||
|
onMoveDown = onMoveDown,
|
||||||
|
onRename = { onClickRename(category) },
|
||||||
|
onDelete = { onClickDelete(category) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
package eu.kanade.presentation.category.components
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
|
||||||
import androidx.compose.foundation.lazy.LazyListState
|
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import eu.kanade.presentation.components.LazyColumn
|
|
||||||
import eu.kanade.presentation.util.padding
|
|
||||||
import tachiyomi.domain.category.model.Category
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun CategoryContent(
|
|
||||||
categories: List<Category>,
|
|
||||||
lazyListState: LazyListState,
|
|
||||||
paddingValues: PaddingValues,
|
|
||||||
onClickRename: (Category) -> Unit,
|
|
||||||
onClickDelete: (Category) -> Unit,
|
|
||||||
onMoveUp: (Category) -> Unit,
|
|
||||||
onMoveDown: (Category) -> Unit,
|
|
||||||
) {
|
|
||||||
LazyColumn(
|
|
||||||
state = lazyListState,
|
|
||||||
contentPadding = paddingValues,
|
|
||||||
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
|
||||||
) {
|
|
||||||
itemsIndexed(
|
|
||||||
items = categories,
|
|
||||||
key = { _, category -> "category-${category.id}" },
|
|
||||||
) { index, category ->
|
|
||||||
CategoryListItem(
|
|
||||||
modifier = Modifier.animateItemPlacement(),
|
|
||||||
category = category,
|
|
||||||
canMoveUp = index != 0,
|
|
||||||
canMoveDown = index != categories.lastIndex,
|
|
||||||
onMoveUp = onMoveUp,
|
|
||||||
onMoveDown = onMoveDown,
|
|
||||||
onRename = { onClickRename(category) },
|
|
||||||
onDelete = { onClickDelete(category) },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,22 +1,38 @@
|
|||||||
package eu.kanade.presentation.category.components
|
package eu.kanade.presentation.category.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.Checkbox
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.OutlinedTextField
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.material3.TriStateCheckbox
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import eu.kanade.core.preference.asToggleableState
|
||||||
|
import eu.kanade.presentation.category.visualName
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
import tachiyomi.core.preference.CheckboxState
|
||||||
import tachiyomi.domain.category.model.Category
|
import tachiyomi.domain.category.model.Category
|
||||||
|
import tachiyomi.presentation.core.components.material.padding
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -97,7 +113,7 @@ fun CategoryRenameDialog(
|
|||||||
onDismissRequest()
|
onDismissRequest()
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
Text(text = stringResource(android.R.string.ok))
|
Text(text = stringResource(R.string.action_ok))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
dismissButton = {
|
dismissButton = {
|
||||||
@@ -146,8 +162,8 @@ fun CategoryDeleteDialog(
|
|||||||
TextButton(onClick = {
|
TextButton(onClick = {
|
||||||
onDelete()
|
onDelete()
|
||||||
onDismissRequest()
|
onDismissRequest()
|
||||||
},) {
|
}) {
|
||||||
Text(text = stringResource(android.R.string.ok))
|
Text(text = stringResource(R.string.action_ok))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
dismissButton = {
|
dismissButton = {
|
||||||
@@ -164,6 +180,140 @@ fun CategoryDeleteDialog(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun List<Category>.anyWithName(name: String): Boolean {
|
@Composable
|
||||||
|
fun CategorySortAlphabeticallyDialog(
|
||||||
|
onDismissRequest: () -> Unit,
|
||||||
|
onSort: () -> Unit,
|
||||||
|
) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(onClick = {
|
||||||
|
onSort()
|
||||||
|
onDismissRequest()
|
||||||
|
}) {
|
||||||
|
Text(text = stringResource(R.string.action_ok))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = onDismissRequest) {
|
||||||
|
Text(text = stringResource(R.string.action_cancel))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
title = {
|
||||||
|
Text(text = stringResource(R.string.action_sort_category))
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
Text(text = stringResource(R.string.sort_category_confirmation))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ChangeCategoryDialog(
|
||||||
|
initialSelection: List<CheckboxState<Category>>,
|
||||||
|
onDismissRequest: () -> Unit,
|
||||||
|
onEditCategories: () -> Unit,
|
||||||
|
onConfirm: (List<Long>, List<Long>) -> Unit,
|
||||||
|
) {
|
||||||
|
if (initialSelection.isEmpty()) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
confirmButton = {
|
||||||
|
tachiyomi.presentation.core.components.material.TextButton(
|
||||||
|
onClick = {
|
||||||
|
onDismissRequest()
|
||||||
|
onEditCategories()
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(R.string.action_edit_categories))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
title = {
|
||||||
|
Text(text = stringResource(R.string.action_move_category))
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
Text(text = stringResource(R.string.information_empty_category_dialog))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var selection by remember { mutableStateOf(initialSelection) }
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
confirmButton = {
|
||||||
|
Row {
|
||||||
|
tachiyomi.presentation.core.components.material.TextButton(onClick = {
|
||||||
|
onDismissRequest()
|
||||||
|
onEditCategories()
|
||||||
|
}) {
|
||||||
|
Text(text = stringResource(R.string.action_edit))
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
tachiyomi.presentation.core.components.material.TextButton(onClick = onDismissRequest) {
|
||||||
|
Text(text = stringResource(R.string.action_cancel))
|
||||||
|
}
|
||||||
|
tachiyomi.presentation.core.components.material.TextButton(
|
||||||
|
onClick = {
|
||||||
|
onDismissRequest()
|
||||||
|
onConfirm(
|
||||||
|
selection.filter { it is CheckboxState.State.Checked || it is CheckboxState.TriState.Include }.map { it.value.id },
|
||||||
|
selection.filter { it is CheckboxState.State.None || it is CheckboxState.TriState.None }.map { it.value.id },
|
||||||
|
)
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(R.string.action_ok))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
title = {
|
||||||
|
Text(text = stringResource(R.string.action_move_category))
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.verticalScroll(rememberScrollState()),
|
||||||
|
) {
|
||||||
|
selection.forEach { checkbox ->
|
||||||
|
val onChange: (CheckboxState<Category>) -> Unit = {
|
||||||
|
val index = selection.indexOf(it)
|
||||||
|
if (index != -1) {
|
||||||
|
val mutableList = selection.toMutableList()
|
||||||
|
mutableList[index] = it.next()
|
||||||
|
selection = mutableList.toList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable { onChange(checkbox) },
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
when (checkbox) {
|
||||||
|
is CheckboxState.TriState -> {
|
||||||
|
TriStateCheckbox(
|
||||||
|
state = checkbox.asToggleableState(),
|
||||||
|
onClick = { onChange(checkbox) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is CheckboxState.State -> {
|
||||||
|
Checkbox(
|
||||||
|
checked = checkbox.isChecked,
|
||||||
|
onCheckedChange = { onChange(checkbox) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = checkbox.value.visualName,
|
||||||
|
modifier = Modifier.padding(horizontal = MaterialTheme.padding.medium),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun List<Category>.anyWithName(name: String): Boolean {
|
||||||
return any { name == it.name }
|
return any { name == it.name }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,10 +7,10 @@ import androidx.compose.material3.Icon
|
|||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import eu.kanade.presentation.components.ExtendedFloatingActionButton
|
|
||||||
import eu.kanade.presentation.util.isScrolledToEnd
|
|
||||||
import eu.kanade.presentation.util.isScrollingUp
|
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
|
import tachiyomi.presentation.core.components.material.ExtendedFloatingActionButton
|
||||||
|
import tachiyomi.presentation.core.util.isScrolledToEnd
|
||||||
|
import tachiyomi.presentation.core.util.isScrollingUp
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun CategoryFloatingActionButton(
|
fun CategoryFloatingActionButton(
|
||||||
|
|||||||
@@ -20,9 +20,9 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import eu.kanade.presentation.util.padding
|
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import tachiyomi.domain.category.model.Category
|
import tachiyomi.domain.category.model.Category
|
||||||
|
import tachiyomi.presentation.core.components.material.padding
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun CategoryListItem(
|
fun CategoryListItem(
|
||||||
|
|||||||
@@ -1,75 +1,25 @@
|
|||||||
package eu.kanade.presentation.components
|
package eu.kanade.presentation.components
|
||||||
|
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
import androidx.compose.animation.with
|
import androidx.compose.animation.togetherWith
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.gestures.Orientation
|
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
|
||||||
import androidx.compose.foundation.layout.WindowInsetsSides
|
|
||||||
import androidx.compose.foundation.layout.asPaddingValues
|
|
||||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.navigationBars
|
|
||||||
import androidx.compose.foundation.layout.offset
|
|
||||||
import androidx.compose.foundation.layout.only
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.requiredWidthIn
|
|
||||||
import androidx.compose.foundation.layout.systemBars
|
|
||||||
import androidx.compose.foundation.layout.systemBarsPadding
|
|
||||||
import androidx.compose.foundation.layout.widthIn
|
|
||||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
|
||||||
import androidx.compose.foundation.shape.ZeroCornerSize
|
|
||||||
import androidx.compose.material.SwipeableState
|
|
||||||
import androidx.compose.material.rememberSwipeableState
|
|
||||||
import androidx.compose.material.swipeable
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Surface
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.runtime.snapshotFlow
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.alpha
|
|
||||||
import androidx.compose.ui.geometry.Offset
|
|
||||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
|
||||||
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.IntOffset
|
|
||||||
import androidx.compose.ui.unit.Velocity
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.window.Dialog
|
||||||
|
import androidx.compose.ui.window.DialogProperties
|
||||||
|
import cafe.adriel.voyager.core.annotation.InternalVoyagerApi
|
||||||
import cafe.adriel.voyager.core.lifecycle.DisposableEffectIgnoringConfiguration
|
import cafe.adriel.voyager.core.lifecycle.DisposableEffectIgnoringConfiguration
|
||||||
import cafe.adriel.voyager.core.screen.Screen
|
import cafe.adriel.voyager.core.screen.Screen
|
||||||
import cafe.adriel.voyager.navigator.Navigator
|
import cafe.adriel.voyager.navigator.Navigator
|
||||||
import cafe.adriel.voyager.transitions.ScreenTransition
|
import eu.kanade.presentation.util.ScreenTransition
|
||||||
import eu.kanade.presentation.util.isTabletUi
|
import eu.kanade.presentation.util.isTabletUi
|
||||||
import kotlinx.coroutines.delay
|
import tachiyomi.presentation.core.components.AdaptiveSheet as AdaptiveSheetImpl
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
|
||||||
import kotlinx.coroutines.flow.drop
|
|
||||||
import kotlinx.coroutines.flow.filter
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlin.math.roundToInt
|
|
||||||
import kotlin.time.Duration.Companion.milliseconds
|
|
||||||
|
|
||||||
private const val SheetAnimationDuration = 500
|
|
||||||
private val SheetAnimationSpec = tween<Float>(durationMillis = SheetAnimationDuration)
|
|
||||||
private const val ScrimAnimationDuration = 350
|
|
||||||
private val ScrimAnimationSpec = tween<Float>(durationMillis = ScrimAnimationDuration)
|
|
||||||
|
|
||||||
|
@OptIn(InternalVoyagerApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun NavigatorAdaptiveSheet(
|
fun NavigatorAdaptiveSheet(
|
||||||
screen: Screen,
|
screen: Screen,
|
||||||
@@ -88,7 +38,7 @@ fun NavigatorAdaptiveSheet(
|
|||||||
ScreenTransition(
|
ScreenTransition(
|
||||||
navigator = sheetNavigator,
|
navigator = sheetNavigator,
|
||||||
transition = {
|
transition = {
|
||||||
fadeIn(animationSpec = tween(220, delayMillis = 90)) with
|
fadeIn(animationSpec = tween(220, delayMillis = 90)) togetherWith
|
||||||
fadeOut(animationSpec = tween(90))
|
fadeOut(animationSpec = tween(90))
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -121,224 +71,31 @@ fun NavigatorAdaptiveSheet(
|
|||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun AdaptiveSheet(
|
fun AdaptiveSheet(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
tonalElevation: Dp = 1.dp,
|
tonalElevation: Dp = 1.dp,
|
||||||
enableSwipeDismiss: Boolean = true,
|
enableSwipeDismiss: Boolean = true,
|
||||||
onDismissRequest: () -> Unit,
|
onDismissRequest: () -> Unit,
|
||||||
content: @Composable (PaddingValues) -> Unit,
|
|
||||||
) {
|
|
||||||
val isTabletUi = isTabletUi()
|
|
||||||
AdaptiveSheetImpl(
|
|
||||||
isTabletUi = isTabletUi,
|
|
||||||
tonalElevation = tonalElevation,
|
|
||||||
enableSwipeDismiss = enableSwipeDismiss,
|
|
||||||
onDismissRequest = onDismissRequest,
|
|
||||||
) {
|
|
||||||
val contentPadding = if (isTabletUi) {
|
|
||||||
PaddingValues()
|
|
||||||
} else {
|
|
||||||
WindowInsets.navigationBars.only(WindowInsetsSides.Bottom).asPaddingValues()
|
|
||||||
}
|
|
||||||
content(contentPadding)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun AdaptiveSheetImpl(
|
|
||||||
isTabletUi: Boolean,
|
|
||||||
tonalElevation: Dp,
|
|
||||||
enableSwipeDismiss: Boolean,
|
|
||||||
onDismissRequest: () -> Unit,
|
|
||||||
content: @Composable () -> Unit,
|
content: @Composable () -> Unit,
|
||||||
) {
|
) {
|
||||||
val scope = rememberCoroutineScope()
|
val isTabletUi = isTabletUi()
|
||||||
if (isTabletUi) {
|
|
||||||
var targetAlpha by remember { mutableStateOf(0f) }
|
|
||||||
val alpha by animateFloatAsState(
|
|
||||||
targetValue = targetAlpha,
|
|
||||||
animationSpec = ScrimAnimationSpec,
|
|
||||||
)
|
|
||||||
val internalOnDismissRequest: () -> Unit = {
|
|
||||||
scope.launch {
|
|
||||||
targetAlpha = 0f
|
|
||||||
delay(ScrimAnimationSpec.durationMillis.milliseconds)
|
|
||||||
onDismissRequest()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
BoxWithConstraints(
|
|
||||||
modifier = Modifier
|
|
||||||
.clickable(
|
|
||||||
enabled = true,
|
|
||||||
interactionSource = remember { MutableInteractionSource() },
|
|
||||||
indication = null,
|
|
||||||
onClick = internalOnDismissRequest,
|
|
||||||
)
|
|
||||||
.fillMaxSize()
|
|
||||||
.alpha(alpha),
|
|
||||||
contentAlignment = Alignment.Center,
|
|
||||||
) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.matchParentSize()
|
|
||||||
.background(MaterialTheme.colorScheme.scrim.copy(alpha = 0.32f)),
|
|
||||||
)
|
|
||||||
Surface(
|
|
||||||
modifier = Modifier
|
|
||||||
.requiredWidthIn(max = 460.dp)
|
|
||||||
.clickable(
|
|
||||||
interactionSource = remember { MutableInteractionSource() },
|
|
||||||
indication = null,
|
|
||||||
onClick = {},
|
|
||||||
)
|
|
||||||
.systemBarsPadding()
|
|
||||||
.padding(vertical = 16.dp),
|
|
||||||
shape = MaterialTheme.shapes.extraLarge,
|
|
||||||
tonalElevation = tonalElevation,
|
|
||||||
content = {
|
|
||||||
BackHandler(enabled = alpha > 0f, onBack = internalOnDismissRequest)
|
|
||||||
content()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
Dialog(
|
||||||
targetAlpha = 1f
|
onDismissRequest = onDismissRequest,
|
||||||
}
|
properties = dialogProperties,
|
||||||
}
|
) {
|
||||||
} else {
|
AdaptiveSheetImpl(
|
||||||
val swipeState = rememberSwipeableState(
|
modifier = modifier,
|
||||||
initialValue = 1,
|
isTabletUi = isTabletUi,
|
||||||
animationSpec = SheetAnimationSpec,
|
tonalElevation = tonalElevation,
|
||||||
)
|
enableSwipeDismiss = enableSwipeDismiss,
|
||||||
val internalOnDismissRequest: () -> Unit = { if (swipeState.currentValue == 0) scope.launch { swipeState.animateTo(1) } }
|
onDismissRequest = onDismissRequest,
|
||||||
BoxWithConstraints(
|
|
||||||
modifier = Modifier
|
|
||||||
.clickable(
|
|
||||||
interactionSource = remember { MutableInteractionSource() },
|
|
||||||
indication = null,
|
|
||||||
onClick = internalOnDismissRequest,
|
|
||||||
)
|
|
||||||
.fillMaxSize(),
|
|
||||||
contentAlignment = Alignment.BottomCenter,
|
|
||||||
) {
|
) {
|
||||||
val fullHeight = constraints.maxHeight.toFloat()
|
content()
|
||||||
val anchors = mapOf(0f to 0, fullHeight to 1)
|
|
||||||
val scrimAlpha by animateFloatAsState(
|
|
||||||
targetValue = if (swipeState.targetValue == 1) 0f else 1f,
|
|
||||||
animationSpec = ScrimAnimationSpec,
|
|
||||||
)
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.matchParentSize()
|
|
||||||
.alpha(scrimAlpha)
|
|
||||||
.background(MaterialTheme.colorScheme.scrim.copy(alpha = 0.32f)),
|
|
||||||
)
|
|
||||||
Surface(
|
|
||||||
modifier = Modifier
|
|
||||||
.widthIn(max = 460.dp)
|
|
||||||
.clickable(
|
|
||||||
interactionSource = remember { MutableInteractionSource() },
|
|
||||||
indication = null,
|
|
||||||
onClick = {},
|
|
||||||
)
|
|
||||||
.nestedScroll(
|
|
||||||
remember(enableSwipeDismiss, anchors) {
|
|
||||||
swipeState.preUpPostDownNestedScrollConnection(
|
|
||||||
enabled = enableSwipeDismiss,
|
|
||||||
anchor = anchors,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.offset {
|
|
||||||
IntOffset(
|
|
||||||
0,
|
|
||||||
swipeState.offset.value.roundToInt(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.swipeable(
|
|
||||||
enabled = enableSwipeDismiss,
|
|
||||||
state = swipeState,
|
|
||||||
anchors = anchors,
|
|
||||||
orientation = Orientation.Vertical,
|
|
||||||
resistance = null,
|
|
||||||
)
|
|
||||||
.windowInsetsPadding(
|
|
||||||
WindowInsets.systemBars
|
|
||||||
.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
|
|
||||||
)
|
|
||||||
.consumeWindowInsets(
|
|
||||||
WindowInsets.systemBars
|
|
||||||
.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
|
|
||||||
),
|
|
||||||
shape = MaterialTheme.shapes.extraLarge.copy(bottomStart = ZeroCornerSize, bottomEnd = ZeroCornerSize),
|
|
||||||
tonalElevation = tonalElevation,
|
|
||||||
content = {
|
|
||||||
BackHandler(enabled = swipeState.targetValue == 0, onBack = internalOnDismissRequest)
|
|
||||||
content()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
LaunchedEffect(swipeState) {
|
|
||||||
scope.launch { swipeState.animateTo(0) }
|
|
||||||
snapshotFlow { swipeState.currentValue }
|
|
||||||
.drop(1)
|
|
||||||
.filter { it == 1 }
|
|
||||||
.collectLatest {
|
|
||||||
delay(ScrimAnimationSpec.durationMillis.milliseconds)
|
|
||||||
onDismissRequest()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private val dialogProperties = DialogProperties(
|
||||||
* Yoinked from Swipeable.kt with modifications to disable
|
usePlatformDefaultWidth = false,
|
||||||
*/
|
decorFitsSystemWindows = false,
|
||||||
private fun <T> SwipeableState<T>.preUpPostDownNestedScrollConnection(
|
)
|
||||||
enabled: Boolean = true,
|
|
||||||
anchor: Map<Float, T>,
|
|
||||||
) = object : NestedScrollConnection {
|
|
||||||
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
|
|
||||||
val delta = available.toFloat()
|
|
||||||
return if (enabled && delta < 0 && source == NestedScrollSource.Drag) {
|
|
||||||
performDrag(delta).toOffset()
|
|
||||||
} else {
|
|
||||||
Offset.Zero
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPostScroll(
|
|
||||||
consumed: Offset,
|
|
||||||
available: Offset,
|
|
||||||
source: NestedScrollSource,
|
|
||||||
): Offset {
|
|
||||||
return if (enabled && source == NestedScrollSource.Drag) {
|
|
||||||
performDrag(available.toFloat()).toOffset()
|
|
||||||
} else {
|
|
||||||
Offset.Zero
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun onPreFling(available: Velocity): Velocity {
|
|
||||||
val toFling = Offset(available.x, available.y).toFloat()
|
|
||||||
return if (enabled && toFling < 0 && offset.value > anchor.keys.minOrNull()!!) {
|
|
||||||
performFling(velocity = toFling)
|
|
||||||
// since we go to the anchor with tween settling, consume all for the best UX
|
|
||||||
available
|
|
||||||
} else {
|
|
||||||
Velocity.Zero
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
|
|
||||||
return if (enabled) {
|
|
||||||
performFling(velocity = Offset(available.x, available.y).toFloat())
|
|
||||||
available
|
|
||||||
} else {
|
|
||||||
Velocity.Zero
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Float.toOffset(): Offset = Offset(0f, this)
|
|
||||||
|
|
||||||
private fun Offset.toFloat(): Float = this.y
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,93 +0,0 @@
|
|||||||
package eu.kanade.presentation.components
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.sizeIn
|
|
||||||
import androidx.compose.material3.LocalContentColor
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.ProvideTextStyle
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun AlertDialogContent(
|
|
||||||
buttons: @Composable () -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
icon: (@Composable () -> Unit)? = null,
|
|
||||||
title: (@Composable () -> Unit)? = null,
|
|
||||||
text: @Composable (() -> Unit)? = null,
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = modifier
|
|
||||||
.sizeIn(minWidth = MinWidth, maxWidth = MaxWidth)
|
|
||||||
.padding(DialogPadding),
|
|
||||||
) {
|
|
||||||
icon?.let {
|
|
||||||
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.secondary) {
|
|
||||||
Box(
|
|
||||||
Modifier
|
|
||||||
.padding(IconPadding)
|
|
||||||
.align(Alignment.CenterHorizontally),
|
|
||||||
) {
|
|
||||||
icon()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
title?.let {
|
|
||||||
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurface) {
|
|
||||||
val textStyle = MaterialTheme.typography.headlineSmall
|
|
||||||
ProvideTextStyle(textStyle) {
|
|
||||||
Box(
|
|
||||||
// Align the title to the center when an icon is present.
|
|
||||||
Modifier
|
|
||||||
.padding(TitlePadding)
|
|
||||||
.align(
|
|
||||||
if (icon == null) {
|
|
||||||
Alignment.Start
|
|
||||||
} else {
|
|
||||||
Alignment.CenterHorizontally
|
|
||||||
},
|
|
||||||
),
|
|
||||||
) {
|
|
||||||
title()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
text?.let {
|
|
||||||
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurfaceVariant) {
|
|
||||||
val textStyle = MaterialTheme.typography.bodyMedium
|
|
||||||
ProvideTextStyle(textStyle) {
|
|
||||||
Box(
|
|
||||||
Modifier
|
|
||||||
.weight(weight = 1f, fill = false)
|
|
||||||
.padding(TextPadding)
|
|
||||||
.align(Alignment.Start),
|
|
||||||
) {
|
|
||||||
text()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Box(modifier = Modifier.align(Alignment.End)) {
|
|
||||||
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.primary) {
|
|
||||||
val textStyle = MaterialTheme.typography.labelLarge
|
|
||||||
ProvideTextStyle(value = textStyle, content = buttons)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Paddings for each of the dialog's parts.
|
|
||||||
private val DialogPadding = PaddingValues(all = 24.dp)
|
|
||||||
private val IconPadding = PaddingValues(bottom = 16.dp)
|
|
||||||
private val TitlePadding = PaddingValues(bottom = 16.dp)
|
|
||||||
private val TextPadding = PaddingValues(bottom = 24.dp)
|
|
||||||
|
|
||||||
private val MinWidth = 280.dp
|
|
||||||
private val MaxWidth = 560.dp
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package eu.kanade.presentation.components
|
package eu.kanade.presentation.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.basicMarquee
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.RowScope
|
import androidx.compose.foundation.layout.RowScope
|
||||||
@@ -10,20 +11,22 @@ import androidx.compose.foundation.text.KeyboardOptions
|
|||||||
import androidx.compose.material.TextFieldDefaults
|
import androidx.compose.material.TextFieldDefaults
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.outlined.ArrowBack
|
import androidx.compose.material.icons.outlined.ArrowBack
|
||||||
|
import androidx.compose.material.icons.outlined.ArrowForward
|
||||||
import androidx.compose.material.icons.outlined.Close
|
import androidx.compose.material.icons.outlined.Close
|
||||||
import androidx.compose.material.icons.outlined.MoreVert
|
import androidx.compose.material.icons.outlined.MoreVert
|
||||||
import androidx.compose.material.icons.outlined.Search
|
import androidx.compose.material.icons.outlined.Search
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.LocalContentColor
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.PlainTooltipBox
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||||
import androidx.compose.material3.surfaceColorAtElevation
|
import androidx.compose.material3.surfaceColorAtElevation
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.key
|
import androidx.compose.runtime.key
|
||||||
@@ -33,32 +36,38 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.SolidColor
|
import androidx.compose.ui.graphics.SolidColor
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.platform.LocalFocusManager
|
import androidx.compose.ui.platform.LocalFocusManager
|
||||||
|
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.input.ImeAction
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
import androidx.compose.ui.text.input.VisualTransformation
|
import androidx.compose.ui.text.input.VisualTransformation
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.LayoutDirection
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import eu.kanade.presentation.util.runOnEnterKeyPressed
|
|
||||||
import eu.kanade.presentation.util.secondaryItemAlpha
|
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
|
import tachiyomi.presentation.core.util.clearFocusOnSoftKeyboardHide
|
||||||
|
import tachiyomi.presentation.core.util.runOnEnterKeyPressed
|
||||||
|
import tachiyomi.presentation.core.util.secondaryItemAlpha
|
||||||
|
import tachiyomi.presentation.core.util.showSoftKeyboard
|
||||||
|
|
||||||
const val SEARCH_DEBOUNCE_MILLIS = 250L
|
const val SEARCH_DEBOUNCE_MILLIS = 250L
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AppBar(
|
fun AppBar(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
backgroundColor: Color? = null,
|
||||||
// Text
|
// Text
|
||||||
title: String?,
|
title: String?,
|
||||||
subtitle: String? = null,
|
subtitle: String? = null,
|
||||||
// Up button
|
// Up button
|
||||||
navigateUp: (() -> Unit)? = null,
|
navigateUp: (() -> Unit)? = null,
|
||||||
navigationIcon: ImageVector = Icons.Outlined.ArrowBack,
|
navigationIcon: ImageVector? = null,
|
||||||
// Menu
|
// Menu
|
||||||
actions: @Composable RowScope.() -> Unit = {},
|
actions: @Composable RowScope.() -> Unit = {},
|
||||||
// Action mode
|
// Action mode
|
||||||
@@ -74,6 +83,7 @@ fun AppBar(
|
|||||||
|
|
||||||
AppBar(
|
AppBar(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
|
backgroundColor = backgroundColor,
|
||||||
titleContent = {
|
titleContent = {
|
||||||
if (isActionMode) {
|
if (isActionMode) {
|
||||||
AppBarTitle(actionModeCounter.toString())
|
AppBarTitle(actionModeCounter.toString())
|
||||||
@@ -99,11 +109,12 @@ fun AppBar(
|
|||||||
@Composable
|
@Composable
|
||||||
fun AppBar(
|
fun AppBar(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
backgroundColor: Color? = null,
|
||||||
// Title
|
// Title
|
||||||
titleContent: @Composable () -> Unit,
|
titleContent: @Composable () -> Unit,
|
||||||
// Up button
|
// Up button
|
||||||
navigateUp: (() -> Unit)? = null,
|
navigateUp: (() -> Unit)? = null,
|
||||||
navigationIcon: ImageVector = Icons.Outlined.ArrowBack,
|
navigationIcon: ImageVector? = null,
|
||||||
// Menu
|
// Menu
|
||||||
actions: @Composable RowScope.() -> Unit = {},
|
actions: @Composable RowScope.() -> Unit = {},
|
||||||
// Action mode
|
// Action mode
|
||||||
@@ -127,18 +138,15 @@ fun AppBar(
|
|||||||
} else {
|
} else {
|
||||||
navigateUp?.let {
|
navigateUp?.let {
|
||||||
IconButton(onClick = it) {
|
IconButton(onClick = it) {
|
||||||
Icon(
|
UpIcon(navigationIcon)
|
||||||
imageVector = navigationIcon,
|
|
||||||
contentDescription = stringResource(R.string.abc_action_bar_up_description),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
title = titleContent,
|
title = titleContent,
|
||||||
actions = actions,
|
actions = actions,
|
||||||
colors = TopAppBarDefaults.smallTopAppBarColors(
|
colors = TopAppBarDefaults.topAppBarColors(
|
||||||
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(
|
containerColor = backgroundColor ?: MaterialTheme.colorScheme.surfaceColorAtElevation(
|
||||||
elevation = if (isActionMode) 3.dp else 0.dp,
|
elevation = if (isActionMode) 3.dp else 0.dp,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -166,6 +174,9 @@ fun AppBarTitle(
|
|||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
modifier = Modifier.basicMarquee(
|
||||||
|
delayMillis = 2_000,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -178,21 +189,37 @@ fun AppBarActions(
|
|||||||
var showMenu by remember { mutableStateOf(false) }
|
var showMenu by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
actions.filterIsInstance<AppBar.Action>().map {
|
actions.filterIsInstance<AppBar.Action>().map {
|
||||||
IconButton(
|
PlainTooltipBox(
|
||||||
onClick = it.onClick,
|
tooltip = { Text(it.title) },
|
||||||
enabled = it.enabled,
|
|
||||||
) {
|
) {
|
||||||
Icon(
|
IconButton(
|
||||||
imageVector = it.icon,
|
onClick = it.onClick,
|
||||||
contentDescription = it.title,
|
enabled = it.enabled,
|
||||||
)
|
modifier = Modifier.tooltipTrigger(),
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = it.icon,
|
||||||
|
tint = it.iconTint ?: LocalContentColor.current,
|
||||||
|
contentDescription = it.title,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val overflowActions = actions.filterIsInstance<AppBar.OverflowAction>()
|
val overflowActions = actions.filterIsInstance<AppBar.OverflowAction>()
|
||||||
if (overflowActions.isNotEmpty()) {
|
if (overflowActions.isNotEmpty()) {
|
||||||
IconButton(onClick = { showMenu = !showMenu }) {
|
PlainTooltipBox(
|
||||||
Icon(Icons.Outlined.MoreVert, contentDescription = stringResource(R.string.abc_action_menu_overflow_description))
|
tooltip = { Text(stringResource(R.string.abc_action_menu_overflow_description)) },
|
||||||
|
) {
|
||||||
|
IconButton(
|
||||||
|
onClick = { showMenu = !showMenu },
|
||||||
|
modifier = Modifier.tooltipTrigger(),
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Outlined.MoreVert,
|
||||||
|
contentDescription = stringResource(R.string.abc_action_menu_overflow_description),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DropdownMenu(
|
DropdownMenu(
|
||||||
@@ -233,7 +260,6 @@ fun SearchToolbar(
|
|||||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||||
) {
|
) {
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
var searchClickCount by remember { mutableStateOf(0) }
|
|
||||||
|
|
||||||
AppBar(
|
AppBar(
|
||||||
titleContent = {
|
titleContent = {
|
||||||
@@ -255,7 +281,9 @@ fun SearchToolbar(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.focusRequester(focusRequester)
|
.focusRequester(focusRequester)
|
||||||
.runOnEnterKeyPressed(action = searchAndClearFocus),
|
.runOnEnterKeyPressed(action = searchAndClearFocus)
|
||||||
|
.showSoftKeyboard(remember { searchQuery.isEmpty() })
|
||||||
|
.clearFocusOnSoftKeyboardHide(),
|
||||||
textStyle = MaterialTheme.typography.titleMedium.copy(
|
textStyle = MaterialTheme.typography.titleMedium.copy(
|
||||||
color = MaterialTheme.colorScheme.onBackground,
|
color = MaterialTheme.colorScheme.onBackground,
|
||||||
fontWeight = FontWeight.Normal,
|
fontWeight = FontWeight.Normal,
|
||||||
@@ -276,18 +304,16 @@ fun SearchToolbar(
|
|||||||
visualTransformation = visualTransformation,
|
visualTransformation = visualTransformation,
|
||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
placeholder = {
|
placeholder = {
|
||||||
(placeholderText ?: stringResource(R.string.action_search_hint)).let { placeholderText ->
|
Text(
|
||||||
Text(
|
modifier = Modifier.secondaryItemAlpha(),
|
||||||
modifier = Modifier.secondaryItemAlpha(),
|
text = (placeholderText ?: stringResource(R.string.action_search_hint)),
|
||||||
text = placeholderText,
|
maxLines = 1,
|
||||||
maxLines = 1,
|
overflow = TextOverflow.Ellipsis,
|
||||||
overflow = TextOverflow.Ellipsis,
|
style = MaterialTheme.typography.titleMedium.copy(
|
||||||
style = MaterialTheme.typography.titleMedium.copy(
|
fontSize = 18.sp,
|
||||||
fontSize = 18.sp,
|
fontWeight = FontWeight.Normal,
|
||||||
fontWeight = FontWeight.Normal,
|
),
|
||||||
),
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -296,20 +322,40 @@ fun SearchToolbar(
|
|||||||
navigateUp = if (searchQuery == null) navigateUp else onClickCloseSearch,
|
navigateUp = if (searchQuery == null) navigateUp else onClickCloseSearch,
|
||||||
actions = {
|
actions = {
|
||||||
key("search") {
|
key("search") {
|
||||||
val onClick = {
|
val onClick = { onChangeSearchQuery("") }
|
||||||
searchClickCount++
|
|
||||||
onChangeSearchQuery("")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!searchEnabled) {
|
if (!searchEnabled) {
|
||||||
// Don't show search action
|
// Don't show search action
|
||||||
} else if (searchQuery == null) {
|
} else if (searchQuery == null) {
|
||||||
IconButton(onClick) {
|
PlainTooltipBox(
|
||||||
Icon(Icons.Outlined.Search, contentDescription = stringResource(R.string.action_search))
|
tooltip = { Text(stringResource(R.string.action_search)) },
|
||||||
|
) {
|
||||||
|
IconButton(
|
||||||
|
onClick = onClick,
|
||||||
|
modifier = Modifier.tooltipTrigger(),
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Outlined.Search,
|
||||||
|
contentDescription = stringResource(R.string.action_search),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if (searchQuery.isNotEmpty()) {
|
} else if (searchQuery.isNotEmpty()) {
|
||||||
IconButton(onClick) {
|
PlainTooltipBox(
|
||||||
Icon(Icons.Outlined.Close, contentDescription = stringResource(R.string.action_reset))
|
tooltip = { Text(stringResource(R.string.action_reset)) },
|
||||||
|
) {
|
||||||
|
IconButton(
|
||||||
|
onClick = {
|
||||||
|
onClick()
|
||||||
|
focusRequester.requestFocus()
|
||||||
|
},
|
||||||
|
modifier = Modifier.tooltipTrigger(),
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Outlined.Close,
|
||||||
|
contentDescription = stringResource(R.string.action_reset),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -319,15 +365,16 @@ fun SearchToolbar(
|
|||||||
isActionMode = false,
|
isActionMode = false,
|
||||||
scrollBehavior = scrollBehavior,
|
scrollBehavior = scrollBehavior,
|
||||||
)
|
)
|
||||||
LaunchedEffect(searchClickCount) {
|
}
|
||||||
if (searchQuery == null) return@LaunchedEffect
|
|
||||||
if (searchClickCount == 0 && searchQuery.isNotEmpty()) return@LaunchedEffect
|
@Composable
|
||||||
try {
|
fun UpIcon(navigationIcon: ImageVector? = null) {
|
||||||
focusRequester.requestFocus()
|
val icon = navigationIcon
|
||||||
} catch (_: Throwable) {
|
?: if (LocalLayoutDirection.current == LayoutDirection.Ltr) Icons.Outlined.ArrowBack else Icons.Outlined.ArrowForward
|
||||||
// TextField is gone
|
Icon(
|
||||||
}
|
imageVector = icon,
|
||||||
}
|
contentDescription = stringResource(R.string.abc_action_bar_up_description),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed interface AppBar {
|
sealed interface AppBar {
|
||||||
@@ -336,6 +383,7 @@ sealed interface AppBar {
|
|||||||
data class Action(
|
data class Action(
|
||||||
val title: String,
|
val title: String,
|
||||||
val icon: ImageVector,
|
val icon: ImageVector,
|
||||||
|
val iconTint: Color? = null,
|
||||||
val onClick: () -> Unit,
|
val onClick: () -> Unit,
|
||||||
val enabled: Boolean = true,
|
val enabled: Boolean = true,
|
||||||
) : AppBarAction
|
) : AppBarAction
|
||||||
|
|||||||
@@ -1,47 +0,0 @@
|
|||||||
package eu.kanade.presentation.components
|
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.RowScope
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.draw.clip
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.graphics.RectangleShape
|
|
||||||
import androidx.compose.ui.graphics.Shape
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun BadgeGroup(
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
shape: Shape = MaterialTheme.shapes.extraSmall,
|
|
||||||
content: @Composable RowScope.() -> Unit,
|
|
||||||
) {
|
|
||||||
Row(modifier = modifier.clip(shape)) {
|
|
||||||
content()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun Badge(
|
|
||||||
text: String,
|
|
||||||
color: Color = MaterialTheme.colorScheme.secondary,
|
|
||||||
textColor: Color = MaterialTheme.colorScheme.onSecondary,
|
|
||||||
shape: Shape = RectangleShape,
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = text,
|
|
||||||
modifier = Modifier
|
|
||||||
.clip(shape)
|
|
||||||
.background(color)
|
|
||||||
.padding(horizontal = 3.dp, vertical = 1.dp),
|
|
||||||
color = textColor,
|
|
||||||
fontWeight = FontWeight.Medium,
|
|
||||||
maxLines = 1,
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,133 +0,0 @@
|
|||||||
package eu.kanade.presentation.components
|
|
||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.rememberScrollState
|
|
||||||
import androidx.compose.foundation.verticalScroll
|
|
||||||
import androidx.compose.material3.AlertDialog
|
|
||||||
import androidx.compose.material3.Checkbox
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.material3.TriStateCheckbox
|
|
||||||
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.res.stringResource
|
|
||||||
import eu.kanade.core.prefs.CheckboxState
|
|
||||||
import eu.kanade.presentation.category.visualName
|
|
||||||
import eu.kanade.presentation.util.padding
|
|
||||||
import eu.kanade.tachiyomi.R
|
|
||||||
import tachiyomi.domain.category.model.Category
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun ChangeCategoryDialog(
|
|
||||||
initialSelection: List<CheckboxState<Category>>,
|
|
||||||
onDismissRequest: () -> Unit,
|
|
||||||
onEditCategories: () -> Unit,
|
|
||||||
onConfirm: (List<Long>, List<Long>) -> Unit,
|
|
||||||
) {
|
|
||||||
if (initialSelection.isEmpty()) {
|
|
||||||
AlertDialog(
|
|
||||||
onDismissRequest = onDismissRequest,
|
|
||||||
confirmButton = {
|
|
||||||
TextButton(
|
|
||||||
onClick = {
|
|
||||||
onDismissRequest()
|
|
||||||
onEditCategories()
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
Text(text = stringResource(R.string.action_edit_categories))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
title = {
|
|
||||||
Text(text = stringResource(R.string.action_move_category))
|
|
||||||
},
|
|
||||||
text = {
|
|
||||||
Text(text = stringResource(R.string.information_empty_category_dialog))
|
|
||||||
},
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var selection by remember { mutableStateOf(initialSelection) }
|
|
||||||
AlertDialog(
|
|
||||||
onDismissRequest = onDismissRequest,
|
|
||||||
confirmButton = {
|
|
||||||
Row {
|
|
||||||
TextButton(onClick = {
|
|
||||||
onDismissRequest()
|
|
||||||
onEditCategories()
|
|
||||||
},) {
|
|
||||||
Text(text = stringResource(R.string.action_edit))
|
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
|
||||||
TextButton(onClick = onDismissRequest) {
|
|
||||||
Text(text = stringResource(R.string.action_cancel))
|
|
||||||
}
|
|
||||||
TextButton(
|
|
||||||
onClick = {
|
|
||||||
onDismissRequest()
|
|
||||||
onConfirm(
|
|
||||||
selection.filter { it is CheckboxState.State.Checked || it is CheckboxState.TriState.Include }.map { it.value.id },
|
|
||||||
selection.filter { it is CheckboxState.State.None || it is CheckboxState.TriState.None }.map { it.value.id },
|
|
||||||
)
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
Text(text = stringResource(android.R.string.ok))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
title = {
|
|
||||||
Text(text = stringResource(R.string.action_move_category))
|
|
||||||
},
|
|
||||||
text = {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier.verticalScroll(rememberScrollState()),
|
|
||||||
) {
|
|
||||||
selection.forEach { checkbox ->
|
|
||||||
val onChange: (CheckboxState<Category>) -> Unit = {
|
|
||||||
val index = selection.indexOf(it)
|
|
||||||
if (index != -1) {
|
|
||||||
val mutableList = selection.toMutableList()
|
|
||||||
mutableList[index] = it.next()
|
|
||||||
selection = mutableList.toList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.clickable { onChange(checkbox) },
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
) {
|
|
||||||
when (checkbox) {
|
|
||||||
is CheckboxState.TriState -> {
|
|
||||||
TriStateCheckbox(
|
|
||||||
state = checkbox.asState(),
|
|
||||||
onClick = { onChange(checkbox) },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
is CheckboxState.State -> {
|
|
||||||
Checkbox(
|
|
||||||
checked = checkbox.isChecked,
|
|
||||||
onCheckedChange = { onChange(checkbox) },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Text(
|
|
||||||
text = checkbox.value.visualName,
|
|
||||||
modifier = Modifier.padding(horizontal = MaterialTheme.padding.medium),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
package eu.kanade.presentation.components
|
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.material3.DividerDefaults
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.draw.alpha
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
|
|
||||||
const val DIVIDER_ALPHA = 0.2f
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun Divider(
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
color: Color = DividerDefaults.color,
|
|
||||||
) {
|
|
||||||
Box(
|
|
||||||
modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.height(1.dp)
|
|
||||||
.background(color = color)
|
|
||||||
.alpha(DIVIDER_ALPHA),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun VerticalDivider(
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
color: Color = DividerDefaults.color,
|
|
||||||
) {
|
|
||||||
Box(
|
|
||||||
modifier
|
|
||||||
.fillMaxHeight()
|
|
||||||
.width(1.dp)
|
|
||||||
.background(color = color)
|
|
||||||
.alpha(DIVIDER_ALPHA),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -3,6 +3,7 @@ package eu.kanade.presentation.components
|
|||||||
import androidx.compose.material3.DropdownMenuItem
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.res.pluralStringResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import eu.kanade.presentation.manga.DownloadAction
|
import eu.kanade.presentation.manga.DownloadAction
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
@@ -12,52 +13,22 @@ fun DownloadDropdownMenu(
|
|||||||
expanded: Boolean,
|
expanded: Boolean,
|
||||||
onDismissRequest: () -> Unit,
|
onDismissRequest: () -> Unit,
|
||||||
onDownloadClicked: (DownloadAction) -> Unit,
|
onDownloadClicked: (DownloadAction) -> Unit,
|
||||||
includeDownloadAllOption: Boolean = true,
|
|
||||||
) {
|
) {
|
||||||
DropdownMenu(
|
DropdownMenu(
|
||||||
expanded = expanded,
|
expanded = expanded,
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
) {
|
) {
|
||||||
DropdownMenuItem(
|
listOfNotNull(
|
||||||
text = { Text(text = stringResource(R.string.download_1)) },
|
DownloadAction.NEXT_1_CHAPTER to pluralStringResource(R.plurals.download_amount, 1, 1),
|
||||||
onClick = {
|
DownloadAction.NEXT_5_CHAPTERS to pluralStringResource(R.plurals.download_amount, 5, 5),
|
||||||
onDownloadClicked(DownloadAction.NEXT_1_CHAPTER)
|
DownloadAction.NEXT_10_CHAPTERS to pluralStringResource(R.plurals.download_amount, 10, 10),
|
||||||
onDismissRequest()
|
DownloadAction.NEXT_25_CHAPTERS to pluralStringResource(R.plurals.download_amount, 25, 25),
|
||||||
},
|
DownloadAction.UNREAD_CHAPTERS to stringResource(R.string.download_unread),
|
||||||
)
|
).map { (downloadAction, string) ->
|
||||||
DropdownMenuItem(
|
|
||||||
text = { Text(text = stringResource(R.string.download_5)) },
|
|
||||||
onClick = {
|
|
||||||
onDownloadClicked(DownloadAction.NEXT_5_CHAPTERS)
|
|
||||||
onDismissRequest()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
DropdownMenuItem(
|
|
||||||
text = { Text(text = stringResource(R.string.download_10)) },
|
|
||||||
onClick = {
|
|
||||||
onDownloadClicked(DownloadAction.NEXT_10_CHAPTERS)
|
|
||||||
onDismissRequest()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
DropdownMenuItem(
|
|
||||||
text = { Text(text = stringResource(R.string.download_custom)) },
|
|
||||||
onClick = {
|
|
||||||
onDownloadClicked(DownloadAction.CUSTOM)
|
|
||||||
onDismissRequest()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
DropdownMenuItem(
|
|
||||||
text = { Text(text = stringResource(R.string.download_unread)) },
|
|
||||||
onClick = {
|
|
||||||
onDownloadClicked(DownloadAction.UNREAD_CHAPTERS)
|
|
||||||
onDismissRequest()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if (includeDownloadAllOption) {
|
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
text = { Text(text = stringResource(R.string.download_all)) },
|
text = { Text(text = string) },
|
||||||
onClick = {
|
onClick = {
|
||||||
onDownloadClicked(DownloadAction.ALL_CHAPTERS)
|
onDownloadClicked(downloadAction)
|
||||||
onDismissRequest()
|
onDismissRequest()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
package eu.kanade.presentation.components
|
package eu.kanade.presentation.components
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.ColumnScope
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
import androidx.compose.foundation.layout.sizeIn
|
import androidx.compose.foundation.layout.sizeIn
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.outlined.MoreVert
|
import androidx.compose.material.icons.outlined.ArrowLeft
|
||||||
|
import androidx.compose.material.icons.outlined.ArrowRight
|
||||||
import androidx.compose.material.icons.outlined.RadioButtonChecked
|
import androidx.compose.material.icons.outlined.RadioButtonChecked
|
||||||
import androidx.compose.material.icons.outlined.RadioButtonUnchecked
|
import androidx.compose.material.icons.outlined.RadioButtonUnchecked
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
@@ -17,13 +16,13 @@ import androidx.compose.runtime.mutableStateOf
|
|||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.DpOffset
|
import androidx.compose.ui.unit.DpOffset
|
||||||
|
import androidx.compose.ui.unit.LayoutDirection
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.window.PopupProperties
|
import androidx.compose.ui.window.PopupProperties
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import me.saket.cascade.CascadeColumnScope
|
|
||||||
import me.saket.cascade.CascadeDropdownMenu
|
|
||||||
import androidx.compose.material3.DropdownMenu as ComposeDropdownMenu
|
import androidx.compose.material3.DropdownMenu as ComposeDropdownMenu
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -72,25 +71,29 @@ fun RadioMenuItem(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun OverflowMenu(
|
fun NestedMenuItem(
|
||||||
content: @Composable CascadeColumnScope.(() -> Unit) -> Unit,
|
text: @Composable () -> Unit,
|
||||||
|
children: @Composable ColumnScope.(() -> Unit) -> Unit,
|
||||||
) {
|
) {
|
||||||
var moreExpanded by remember { mutableStateOf(false) }
|
var nestedExpanded by remember { mutableStateOf(false) }
|
||||||
val closeMenu = { moreExpanded = false }
|
val closeMenu = { nestedExpanded = false }
|
||||||
|
val isLtr = LocalLayoutDirection.current == LayoutDirection.Ltr
|
||||||
|
|
||||||
Box {
|
DropdownMenuItem(
|
||||||
IconButton(onClick = { moreExpanded = !moreExpanded }) {
|
text = text,
|
||||||
|
onClick = { nestedExpanded = true },
|
||||||
|
trailingIcon = {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Outlined.MoreVert,
|
imageVector = if (isLtr) Icons.Outlined.ArrowRight else Icons.Outlined.ArrowLeft,
|
||||||
contentDescription = stringResource(R.string.abc_action_menu_overflow_description),
|
contentDescription = null,
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
CascadeDropdownMenu(
|
)
|
||||||
expanded = moreExpanded,
|
|
||||||
onDismissRequest = closeMenu,
|
DropdownMenu(
|
||||||
offset = DpOffset(8.dp, (-56).dp),
|
expanded = nestedExpanded,
|
||||||
) {
|
onDismissRequest = closeMenu,
|
||||||
content(closeMenu)
|
) {
|
||||||
}
|
children(closeMenu)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,124 +1,15 @@
|
|||||||
package eu.kanade.presentation.components
|
package eu.kanade.presentation.components
|
||||||
|
|
||||||
import androidx.annotation.StringRes
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.paddingFromBaseline
|
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.outlined.HelpOutline
|
import androidx.compose.material.icons.outlined.HelpOutline
|
||||||
import androidx.compose.material.icons.outlined.Refresh
|
import androidx.compose.material.icons.outlined.Refresh
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.material3.TextButton
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import eu.kanade.presentation.theme.TachiyomiTheme
|
import eu.kanade.presentation.theme.TachiyomiTheme
|
||||||
import eu.kanade.presentation.util.ThemePreviews
|
|
||||||
import eu.kanade.presentation.util.padding
|
|
||||||
import eu.kanade.presentation.util.secondaryItemAlpha
|
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import kotlin.random.Random
|
import tachiyomi.presentation.core.screens.EmptyScreen
|
||||||
|
import tachiyomi.presentation.core.screens.EmptyScreenAction
|
||||||
@Composable
|
import tachiyomi.presentation.core.util.ThemePreviews
|
||||||
fun EmptyScreen(
|
|
||||||
@StringRes textResource: Int,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
actions: List<EmptyScreenAction>? = null,
|
|
||||||
) {
|
|
||||||
EmptyScreen(
|
|
||||||
message = stringResource(textResource),
|
|
||||||
modifier = modifier,
|
|
||||||
actions = actions,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun EmptyScreen(
|
|
||||||
message: String,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
actions: List<EmptyScreenAction>? = null,
|
|
||||||
) {
|
|
||||||
val face = remember { getRandomErrorFace() }
|
|
||||||
Column(
|
|
||||||
modifier = modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.padding(horizontal = 24.dp),
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
verticalArrangement = Arrangement.Center,
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = face,
|
|
||||||
modifier = Modifier.secondaryItemAlpha(),
|
|
||||||
style = MaterialTheme.typography.displayMedium,
|
|
||||||
)
|
|
||||||
|
|
||||||
Text(
|
|
||||||
text = message,
|
|
||||||
modifier = Modifier.paddingFromBaseline(top = 24.dp).secondaryItemAlpha(),
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
textAlign = TextAlign.Center,
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!actions.isNullOrEmpty()) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(
|
|
||||||
top = 24.dp,
|
|
||||||
start = 24.dp,
|
|
||||||
end = 24.dp,
|
|
||||||
),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
|
||||||
) {
|
|
||||||
actions.forEach {
|
|
||||||
ActionButton(
|
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
title = stringResource(it.stringResId),
|
|
||||||
icon = it.icon,
|
|
||||||
onClick = it.onClick,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun ActionButton(
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
title: String,
|
|
||||||
icon: ImageVector,
|
|
||||||
onClick: () -> Unit,
|
|
||||||
) {
|
|
||||||
TextButton(
|
|
||||||
modifier = modifier,
|
|
||||||
onClick = onClick,
|
|
||||||
) {
|
|
||||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
|
||||||
Icon(
|
|
||||||
imageVector = icon,
|
|
||||||
contentDescription = null,
|
|
||||||
)
|
|
||||||
Spacer(Modifier.height(4.dp))
|
|
||||||
Text(
|
|
||||||
text = title,
|
|
||||||
textAlign = TextAlign.Center,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ThemePreviews
|
@ThemePreviews
|
||||||
@Composable
|
@Composable
|
||||||
@@ -155,22 +46,3 @@ private fun WithActionPreview() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class EmptyScreenAction(
|
|
||||||
@StringRes val stringResId: Int,
|
|
||||||
val icon: ImageVector,
|
|
||||||
val onClick: () -> Unit,
|
|
||||||
)
|
|
||||||
|
|
||||||
private val ERROR_FACES = listOf(
|
|
||||||
"(・o・;)",
|
|
||||||
"Σ(ಠ_ಠ)",
|
|
||||||
"ಥ_ಥ",
|
|
||||||
"(˘・_・˘)",
|
|
||||||
"(; ̄Д ̄)",
|
|
||||||
"(・Д・。",
|
|
||||||
)
|
|
||||||
|
|
||||||
private fun getRandomErrorFace(): String {
|
|
||||||
return ERROR_FACES[Random.nextInt(ERROR_FACES.size)]
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,259 +0,0 @@
|
|||||||
package eu.kanade.presentation.components
|
|
||||||
|
|
||||||
import androidx.compose.foundation.gestures.FlingBehavior
|
|
||||||
import androidx.compose.foundation.gestures.Orientation
|
|
||||||
import androidx.compose.foundation.gestures.snapping.SnapLayoutInfoProvider
|
|
||||||
import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.BoxScope
|
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
|
||||||
import androidx.compose.foundation.layout.wrapContentSize
|
|
||||||
import androidx.compose.foundation.lazy.LazyListItemInfo
|
|
||||||
import androidx.compose.foundation.lazy.LazyListLayoutInfo
|
|
||||||
import androidx.compose.foundation.lazy.LazyListState
|
|
||||||
import androidx.compose.foundation.lazy.LazyRow
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.Stable
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.saveable.Saver
|
|
||||||
import androidx.compose.runtime.saveable.listSaver
|
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.runtime.snapshotFlow
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.unit.Density
|
|
||||||
import androidx.compose.ui.util.fastForEach
|
|
||||||
import androidx.compose.ui.util.fastMaxBy
|
|
||||||
import androidx.compose.ui.util.fastSumBy
|
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun HorizontalPager(
|
|
||||||
count: Int,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
state: PagerState = rememberPagerState(),
|
|
||||||
key: ((page: Int) -> Any)? = null,
|
|
||||||
contentPadding: PaddingValues = PaddingValues(),
|
|
||||||
verticalAlignment: Alignment.Vertical = Alignment.CenterVertically,
|
|
||||||
userScrollEnabled: Boolean = true,
|
|
||||||
content: @Composable BoxScope.(page: Int) -> Unit,
|
|
||||||
) {
|
|
||||||
Pager(
|
|
||||||
count = count,
|
|
||||||
modifier = modifier,
|
|
||||||
state = state,
|
|
||||||
isVertical = false,
|
|
||||||
key = key,
|
|
||||||
contentPadding = contentPadding,
|
|
||||||
verticalAlignment = verticalAlignment,
|
|
||||||
userScrollEnabled = userScrollEnabled,
|
|
||||||
content = content,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun Pager(
|
|
||||||
count: Int,
|
|
||||||
modifier: Modifier,
|
|
||||||
state: PagerState,
|
|
||||||
isVertical: Boolean,
|
|
||||||
key: ((page: Int) -> Any)?,
|
|
||||||
contentPadding: PaddingValues,
|
|
||||||
userScrollEnabled: Boolean,
|
|
||||||
verticalAlignment: Alignment.Vertical = Alignment.CenterVertically,
|
|
||||||
horizontalAlignment: Alignment.Horizontal = Alignment.CenterHorizontally,
|
|
||||||
content: @Composable BoxScope.(page: Int) -> Unit,
|
|
||||||
) {
|
|
||||||
LaunchedEffect(count) {
|
|
||||||
state.currentPage = minOf(count - 1, state.currentPage).coerceAtLeast(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(state) {
|
|
||||||
snapshotFlow { state.mostVisiblePageLayoutInfo?.index }
|
|
||||||
.distinctUntilChanged()
|
|
||||||
.collect { state.updateCurrentPageBasedOnLazyListState() }
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isVertical) {
|
|
||||||
LazyColumn(
|
|
||||||
modifier = modifier,
|
|
||||||
state = state.lazyListState,
|
|
||||||
contentPadding = contentPadding,
|
|
||||||
horizontalAlignment = horizontalAlignment,
|
|
||||||
verticalArrangement = Arrangement.aligned(verticalAlignment),
|
|
||||||
userScrollEnabled = userScrollEnabled,
|
|
||||||
flingBehavior = rememberLazyListSnapFlingBehavior(lazyListState = state.lazyListState),
|
|
||||||
) {
|
|
||||||
items(
|
|
||||||
count = count,
|
|
||||||
key = key,
|
|
||||||
) { page ->
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillParentMaxHeight()
|
|
||||||
.wrapContentSize(),
|
|
||||||
) {
|
|
||||||
content(this, page)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
LazyRow(
|
|
||||||
modifier = modifier,
|
|
||||||
state = state.lazyListState,
|
|
||||||
contentPadding = contentPadding,
|
|
||||||
verticalAlignment = verticalAlignment,
|
|
||||||
horizontalArrangement = Arrangement.aligned(horizontalAlignment),
|
|
||||||
userScrollEnabled = userScrollEnabled,
|
|
||||||
flingBehavior = rememberLazyListSnapFlingBehavior(lazyListState = state.lazyListState),
|
|
||||||
) {
|
|
||||||
items(
|
|
||||||
count = count,
|
|
||||||
key = key,
|
|
||||||
) { page ->
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillParentMaxWidth()
|
|
||||||
.wrapContentSize(),
|
|
||||||
) {
|
|
||||||
content(this, page)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun rememberPagerState(
|
|
||||||
initialPage: Int = 0,
|
|
||||||
) = rememberSaveable(saver = PagerState.Saver) {
|
|
||||||
PagerState(currentPage = initialPage)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Stable
|
|
||||||
class PagerState(
|
|
||||||
currentPage: Int = 0,
|
|
||||||
) {
|
|
||||||
init { check(currentPage >= 0) { "currentPage cannot be less than zero" } }
|
|
||||||
|
|
||||||
val lazyListState = LazyListState(firstVisibleItemIndex = currentPage)
|
|
||||||
|
|
||||||
private var _currentPage by mutableStateOf(currentPage)
|
|
||||||
|
|
||||||
var currentPage: Int
|
|
||||||
get() = _currentPage
|
|
||||||
set(value) {
|
|
||||||
if (value != _currentPage) {
|
|
||||||
_currentPage = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val mostVisiblePageLayoutInfo: LazyListItemInfo?
|
|
||||||
get() {
|
|
||||||
val layoutInfo = lazyListState.layoutInfo
|
|
||||||
return layoutInfo.visibleItemsInfo.fastMaxBy {
|
|
||||||
val start = maxOf(it.offset, 0)
|
|
||||||
val end = minOf(
|
|
||||||
it.offset + it.size,
|
|
||||||
layoutInfo.viewportEndOffset - layoutInfo.afterContentPadding,
|
|
||||||
)
|
|
||||||
end - start
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateCurrentPageBasedOnLazyListState() {
|
|
||||||
mostVisiblePageLayoutInfo?.let {
|
|
||||||
currentPage = it.index
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun animateScrollToPage(page: Int) {
|
|
||||||
lazyListState.animateScrollToItem(index = page)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun scrollToPage(page: Int) {
|
|
||||||
lazyListState.scrollToItem(index = page)
|
|
||||||
updateCurrentPageBasedOnLazyListState()
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
val Saver: Saver<PagerState, *> = listSaver(
|
|
||||||
save = { listOf(it.currentPage) },
|
|
||||||
restore = { PagerState(it[0]) },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://android.googlesource.com/platform/frameworks/support/+/refs/changes/78/2160778/35/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/snapping/LazyListSnapLayoutInfoProvider.kt
|
|
||||||
private fun lazyListSnapLayoutInfoProvider(
|
|
||||||
lazyListState: LazyListState,
|
|
||||||
positionInLayout: (layoutSize: Float, itemSize: Float) -> Float = { layoutSize, itemSize ->
|
|
||||||
layoutSize / 2f - itemSize / 2f
|
|
||||||
},
|
|
||||||
) = object : SnapLayoutInfoProvider {
|
|
||||||
|
|
||||||
private val layoutInfo: LazyListLayoutInfo
|
|
||||||
get() = lazyListState.layoutInfo
|
|
||||||
|
|
||||||
// Single page snapping is the default
|
|
||||||
override fun Density.calculateApproachOffset(initialVelocity: Float): Float = 0f
|
|
||||||
|
|
||||||
override fun Density.calculateSnappingOffsetBounds(): ClosedFloatingPointRange<Float> {
|
|
||||||
var lowerBoundOffset = Float.NEGATIVE_INFINITY
|
|
||||||
var upperBoundOffset = Float.POSITIVE_INFINITY
|
|
||||||
|
|
||||||
layoutInfo.visibleItemsInfo.fastForEach { item ->
|
|
||||||
val offset =
|
|
||||||
calculateDistanceToDesiredSnapPosition(layoutInfo, item, positionInLayout)
|
|
||||||
|
|
||||||
// Find item that is closest to the center
|
|
||||||
if (offset <= 0 && offset > lowerBoundOffset) {
|
|
||||||
lowerBoundOffset = offset
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find item that is closest to center, but after it
|
|
||||||
if (offset >= 0 && offset < upperBoundOffset) {
|
|
||||||
upperBoundOffset = offset
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return lowerBoundOffset.rangeTo(upperBoundOffset)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun Density.calculateSnapStepSize(): Float = with(layoutInfo) {
|
|
||||||
if (visibleItemsInfo.isNotEmpty()) {
|
|
||||||
visibleItemsInfo.fastSumBy { it.size } / visibleItemsInfo.size.toFloat()
|
|
||||||
} else {
|
|
||||||
0f
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun rememberLazyListSnapFlingBehavior(lazyListState: LazyListState): FlingBehavior {
|
|
||||||
val snappingLayout = remember(lazyListState) { lazyListSnapLayoutInfoProvider(lazyListState) }
|
|
||||||
return rememberSnapFlingBehavior(snappingLayout)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun calculateDistanceToDesiredSnapPosition(
|
|
||||||
layoutInfo: LazyListLayoutInfo,
|
|
||||||
item: LazyListItemInfo,
|
|
||||||
positionInLayout: (layoutSize: Float, itemSize: Float) -> Float,
|
|
||||||
): Float {
|
|
||||||
val containerSize =
|
|
||||||
with(layoutInfo) { singleAxisViewportSize - beforeContentPadding - afterContentPadding }
|
|
||||||
|
|
||||||
val desiredDistance =
|
|
||||||
positionInLayout(containerSize.toFloat(), item.size.toFloat())
|
|
||||||
|
|
||||||
val itemCurrentPosition = item.offset
|
|
||||||
return itemCurrentPosition - desiredDistance
|
|
||||||
}
|
|
||||||
|
|
||||||
private val LazyListLayoutInfo.singleAxisViewportSize: Int
|
|
||||||
get() = if (orientation == Orientation.Vertical) viewportSize.height else viewportSize.width
|
|
||||||
@@ -5,6 +5,7 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import eu.kanade.tachiyomi.util.lang.toRelativeString
|
import eu.kanade.tachiyomi.util.lang.toRelativeString
|
||||||
|
import tachiyomi.presentation.core.components.ListGroupHeader
|
||||||
import java.text.DateFormat
|
import java.text.DateFormat
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
|
||||||
@@ -12,7 +13,7 @@ import java.util.Date
|
|||||||
fun RelativeDateHeader(
|
fun RelativeDateHeader(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
date: Date,
|
date: Date,
|
||||||
relativeTime: Int,
|
relativeTime: Boolean,
|
||||||
dateFormat: DateFormat,
|
dateFormat: DateFormat,
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|||||||
@@ -1,135 +0,0 @@
|
|||||||
package eu.kanade.presentation.components
|
|
||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.material.ContentAlpha
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.filled.ArrowDownward
|
|
||||||
import androidx.compose.material.icons.filled.ArrowUpward
|
|
||||||
import androidx.compose.material.icons.rounded.CheckBox
|
|
||||||
import androidx.compose.material.icons.rounded.CheckBoxOutlineBlank
|
|
||||||
import androidx.compose.material.icons.rounded.DisabledByDefault
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.RadioButton
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import tachiyomi.domain.manga.model.TriStateFilter
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun TriStateItem(
|
|
||||||
label: String,
|
|
||||||
state: TriStateFilter,
|
|
||||||
onClick: ((TriStateFilter) -> Unit)?,
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.clickable(
|
|
||||||
enabled = onClick != null,
|
|
||||||
onClick = {
|
|
||||||
when (state) {
|
|
||||||
TriStateFilter.DISABLED -> onClick?.invoke(TriStateFilter.ENABLED_IS)
|
|
||||||
TriStateFilter.ENABLED_IS -> onClick?.invoke(TriStateFilter.ENABLED_NOT)
|
|
||||||
TriStateFilter.ENABLED_NOT -> onClick?.invoke(TriStateFilter.DISABLED)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = TabbedDialogPaddings.Horizontal, vertical = 12.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(24.dp),
|
|
||||||
) {
|
|
||||||
val stateAlpha = if (onClick != null) 1f else ContentAlpha.disabled
|
|
||||||
|
|
||||||
Icon(
|
|
||||||
imageVector = when (state) {
|
|
||||||
TriStateFilter.DISABLED -> Icons.Rounded.CheckBoxOutlineBlank
|
|
||||||
TriStateFilter.ENABLED_IS -> Icons.Rounded.CheckBox
|
|
||||||
TriStateFilter.ENABLED_NOT -> Icons.Rounded.DisabledByDefault
|
|
||||||
},
|
|
||||||
contentDescription = null,
|
|
||||||
tint = if (state == TriStateFilter.DISABLED) {
|
|
||||||
MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = stateAlpha)
|
|
||||||
} else {
|
|
||||||
when (onClick) {
|
|
||||||
null -> MaterialTheme.colorScheme.onSurface.copy(alpha = ContentAlpha.disabled)
|
|
||||||
else -> MaterialTheme.colorScheme.primary
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = label,
|
|
||||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = stateAlpha),
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun SortItem(
|
|
||||||
label: String,
|
|
||||||
sortDescending: Boolean?,
|
|
||||||
onClick: () -> Unit,
|
|
||||||
) {
|
|
||||||
val arrowIcon = when (sortDescending) {
|
|
||||||
true -> Icons.Default.ArrowDownward
|
|
||||||
false -> Icons.Default.ArrowUpward
|
|
||||||
null -> null
|
|
||||||
}
|
|
||||||
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.clickable(onClick = onClick)
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = TabbedDialogPaddings.Horizontal, vertical = 12.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(24.dp),
|
|
||||||
) {
|
|
||||||
if (arrowIcon != null) {
|
|
||||||
Icon(
|
|
||||||
imageVector = arrowIcon,
|
|
||||||
contentDescription = null,
|
|
||||||
tint = MaterialTheme.colorScheme.primary,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
Spacer(modifier = Modifier.size(24.dp))
|
|
||||||
}
|
|
||||||
Text(
|
|
||||||
text = label,
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun RadioItem(
|
|
||||||
label: String,
|
|
||||||
selected: Boolean,
|
|
||||||
onClick: () -> Unit,
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.clickable(onClick = onClick)
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = TabbedDialogPaddings.Horizontal, vertical = 12.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(24.dp),
|
|
||||||
) {
|
|
||||||
RadioButton(
|
|
||||||
selected = selected,
|
|
||||||
onClick = null,
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = label,
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,36 +1,37 @@
|
|||||||
package eu.kanade.presentation.components
|
package eu.kanade.presentation.components
|
||||||
|
|
||||||
|
import androidx.compose.animation.animateContentSize
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.ColumnScope
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.heightIn
|
|
||||||
import androidx.compose.foundation.layout.wrapContentSize
|
import androidx.compose.foundation.layout.wrapContentSize
|
||||||
|
import androidx.compose.foundation.pager.PagerState
|
||||||
|
import androidx.compose.foundation.pager.rememberPagerState
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.MoreVert
|
import androidx.compose.material.icons.filled.MoreVert
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Tab
|
import androidx.compose.material3.Tab
|
||||||
import androidx.compose.material3.TabRow
|
import androidx.compose.material3.TabRow
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.layout.onSizeChanged
|
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.util.fastForEachIndexed
|
import androidx.compose.ui.util.fastForEachIndexed
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import tachiyomi.presentation.core.components.HorizontalPager
|
||||||
|
import tachiyomi.presentation.core.components.material.TabIndicator
|
||||||
|
import tachiyomi.presentation.core.components.material.TabText
|
||||||
|
|
||||||
object TabbedDialogPaddings {
|
object TabbedDialogPaddings {
|
||||||
val Horizontal = 24.dp
|
val Horizontal = 24.dp
|
||||||
@@ -39,68 +40,47 @@ object TabbedDialogPaddings {
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TabbedDialog(
|
fun TabbedDialog(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
onDismissRequest: () -> Unit,
|
onDismissRequest: () -> Unit,
|
||||||
tabTitles: List<String>,
|
tabTitles: List<String>,
|
||||||
tabOverflowMenuContent: (@Composable ColumnScope.(() -> Unit) -> Unit)? = null,
|
tabOverflowMenuContent: (@Composable ColumnScope.(() -> Unit) -> Unit)? = null,
|
||||||
content: @Composable (PaddingValues, Int) -> Unit,
|
pagerState: PagerState = rememberPagerState { tabTitles.size },
|
||||||
|
content: @Composable (Int) -> Unit,
|
||||||
) {
|
) {
|
||||||
AdaptiveSheet(
|
AdaptiveSheet(
|
||||||
|
modifier = modifier,
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
) { contentPadding ->
|
) {
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val pagerState = rememberPagerState()
|
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
Row {
|
Row {
|
||||||
TabRow(
|
TabRow(
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
selectedTabIndex = pagerState.currentPage,
|
selectedTabIndex = pagerState.currentPage,
|
||||||
indicator = { TabIndicator(it[pagerState.currentPage]) },
|
indicator = { TabIndicator(it[pagerState.currentPage], pagerState.currentPageOffsetFraction) },
|
||||||
divider = {},
|
divider = {},
|
||||||
) {
|
) {
|
||||||
tabTitles.fastForEachIndexed { i, tab ->
|
tabTitles.fastForEachIndexed { index, tab ->
|
||||||
val selected = pagerState.currentPage == i
|
|
||||||
Tab(
|
Tab(
|
||||||
selected = selected,
|
selected = pagerState.currentPage == index,
|
||||||
onClick = { scope.launch { pagerState.animateScrollToPage(i) } },
|
onClick = { scope.launch { pagerState.animateScrollToPage(index) } },
|
||||||
text = {
|
text = { TabText(text = tab) },
|
||||||
Text(
|
unselectedContentColor = MaterialTheme.colorScheme.onSurface,
|
||||||
text = tab,
|
|
||||||
color = if (selected) {
|
|
||||||
MaterialTheme.colorScheme.primary
|
|
||||||
} else {
|
|
||||||
MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
},
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tabOverflowMenuContent?.let { MoreMenu(it) }
|
tabOverflowMenuContent?.let { MoreMenu(it) }
|
||||||
}
|
}
|
||||||
Divider()
|
HorizontalDivider()
|
||||||
|
|
||||||
val density = LocalDensity.current
|
|
||||||
var largestHeight by rememberSaveable { mutableStateOf(0f) }
|
|
||||||
HorizontalPager(
|
HorizontalPager(
|
||||||
modifier = Modifier.heightIn(min = largestHeight.dp),
|
modifier = Modifier.animateContentSize(),
|
||||||
count = tabTitles.size,
|
|
||||||
state = pagerState,
|
state = pagerState,
|
||||||
verticalAlignment = Alignment.Top,
|
verticalAlignment = Alignment.Top,
|
||||||
) { page ->
|
) { page ->
|
||||||
Box(
|
content(page)
|
||||||
modifier = Modifier.onSizeChanged {
|
|
||||||
with(density) {
|
|
||||||
val heightDp = it.height.toDp()
|
|
||||||
if (heightDp.value > largestHeight) {
|
|
||||||
largestHeight = heightDp.value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
content(contentPadding, page)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.calculateEndPadding
|
|||||||
import androidx.compose.foundation.layout.calculateStartPadding
|
import androidx.compose.foundation.layout.calculateStartPadding
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.pager.rememberPagerState
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.SnackbarHost
|
import androidx.compose.material3.SnackbarHost
|
||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material3.SnackbarHostState
|
||||||
@@ -21,6 +22,10 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import tachiyomi.presentation.core.components.HorizontalPager
|
||||||
|
import tachiyomi.presentation.core.components.material.Scaffold
|
||||||
|
import tachiyomi.presentation.core.components.material.TabIndicator
|
||||||
|
import tachiyomi.presentation.core.components.material.TabText
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TabbedScreen(
|
fun TabbedScreen(
|
||||||
@@ -31,7 +36,7 @@ fun TabbedScreen(
|
|||||||
onChangeSearchQuery: (String?) -> Unit = {},
|
onChangeSearchQuery: (String?) -> Unit = {},
|
||||||
) {
|
) {
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val state = rememberPagerState()
|
val state = rememberPagerState { tabs.size }
|
||||||
val snackbarHostState = remember { SnackbarHostState() }
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
|
|
||||||
LaunchedEffect(startIndex) {
|
LaunchedEffect(startIndex) {
|
||||||
@@ -64,7 +69,7 @@ fun TabbedScreen(
|
|||||||
) {
|
) {
|
||||||
TabRow(
|
TabRow(
|
||||||
selectedTabIndex = state.currentPage,
|
selectedTabIndex = state.currentPage,
|
||||||
indicator = { TabIndicator(it[state.currentPage]) },
|
indicator = { TabIndicator(it[state.currentPage], state.currentPageOffsetFraction) },
|
||||||
) {
|
) {
|
||||||
tabs.forEachIndexed { index, tab ->
|
tabs.forEachIndexed { index, tab ->
|
||||||
Tab(
|
Tab(
|
||||||
@@ -77,7 +82,6 @@ fun TabbedScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
HorizontalPager(
|
HorizontalPager(
|
||||||
count = tabs.size,
|
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
state = state,
|
state = state,
|
||||||
verticalAlignment = Alignment.Top,
|
verticalAlignment = Alignment.Top,
|
||||||
|
|||||||
@@ -1,48 +0,0 @@
|
|||||||
package eu.kanade.presentation.components
|
|
||||||
|
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.TabPosition
|
|
||||||
import androidx.compose.material3.TabRowDefaults
|
|
||||||
import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.draw.clip
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun TabIndicator(currentTabPosition: TabPosition) {
|
|
||||||
TabRowDefaults.Indicator(
|
|
||||||
Modifier
|
|
||||||
.tabIndicatorOffset(currentTabPosition)
|
|
||||||
.padding(horizontal = 8.dp)
|
|
||||||
.clip(RoundedCornerShape(topStart = 3.dp, topEnd = 3.dp)),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun TabText(
|
|
||||||
text: String,
|
|
||||||
badgeCount: Int? = null,
|
|
||||||
) {
|
|
||||||
val pillAlpha = if (isSystemInDarkTheme()) 0.12f else 0.08f
|
|
||||||
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
) {
|
|
||||||
Text(text = text)
|
|
||||||
if (badgeCount != null) {
|
|
||||||
Pill(
|
|
||||||
text = "$badgeCount",
|
|
||||||
color = MaterialTheme.colorScheme.onBackground.copy(alpha = pillAlpha),
|
|
||||||
fontSize = 10.sp,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user