mirror of
https://github.com/mihonapp/mihon.git
synced 2025-07-27 18:05:53 +02:00
Compare commits
918 Commits
Author | SHA1 | Date | |
---|---|---|---|
49991d38d9 | |||
33c62ab711 | |||
899bd26956 | |||
a37f3eb709 | |||
9ae71dfe93 | |||
c65a9aecf5 | |||
02e50411de | |||
6e822dfd5b | |||
7292dadd5f | |||
b1067b942e | |||
d6c4af89c4 | |||
cf6f7c521c | |||
c6601c1f94 | |||
68899aea61 | |||
c3edf9b5d0 | |||
97e04392d3 | |||
3d178737b1 | |||
bf737cf95c | |||
c91ec9a33b | |||
a8040cb21a | |||
f60782f11f | |||
7d6e1bdafc | |||
5854ad97e0 | |||
4b8fa059d5 | |||
3dc2f9a711 | |||
8033a94ee2 | |||
028da099dd | |||
e6c6c32d81 | |||
bce6af62fc | |||
6510a9617a | |||
14510f1d26 | |||
f115edf2ea | |||
8a8362203f | |||
f3336fc5c3 | |||
727289c8eb | |||
9c91ddd4e3 | |||
3ea026e311 | |||
36f307e3bb | |||
89678ebb17 | |||
80b7d14af1 | |||
bbd8098a61 | |||
f8ef0f143b | |||
a3ef3604ee | |||
c4ceda59df | |||
7e053b5862 | |||
ac8ed3c028 | |||
8321ff6000 | |||
9c899e97a9 | |||
556f5a42a7 | |||
850813820c | |||
dba5e6fbfd | |||
c17ada2c98 | |||
32bed9b041 | |||
e0a0942015 | |||
8409ebe4eb | |||
493da5c3f4 | |||
4e221397ce | |||
8a7d6a328a | |||
22589a9c30 | |||
ec478cbb1b | |||
b5e3f429fc | |||
83130f9bf9 | |||
6f34c5e894 | |||
74931fad86 | |||
6ab8e1e73d | |||
1cdaa761b7 | |||
901b77f55c | |||
54f4711f7b | |||
3d0d5c0472 | |||
f0a0ecfd4a | |||
f3b7eaf4a3 | |||
5bba7af24a | |||
32c3269291 | |||
a1e84911be | |||
6bb77bcf1a | |||
ccec5c3efe | |||
8735836498 | |||
8b65fd5751 | |||
f0710df356 | |||
3afcee81f4 | |||
9c120e6231 | |||
4b208fc7ce | |||
a9b0ac43c4 | |||
fca4f25122 | |||
bfb0d31ff6 | |||
8939274b5c | |||
087da2b2f3 | |||
4571dc6b56 | |||
f31bc47757 | |||
950b4a6c90 | |||
2d7650537d | |||
80d6d412f3 | |||
446b146f95 | |||
6887d98f15 | |||
6d74a86711 | |||
5908bd1930 | |||
1a559124eb | |||
54ba1d719e | |||
93cbeca5c0 | |||
19f0175a56 | |||
bf3899d04a | |||
dcf0379496 | |||
9f90ee358b | |||
565317d99c | |||
83a67feb48 | |||
a51108cbe8 | |||
b9fd416fc6 | |||
c10cd6c808 | |||
c62cd6e997 | |||
7ae17e6aac | |||
f20980b4c9 | |||
02cd2d2ca3 | |||
3847d4f4cf | |||
f9b57800b1 | |||
387159b5af | |||
09531e7f5a | |||
c6356fe4b2 | |||
ff3bc66055 | |||
13b3bec8ad | |||
8aaf8df708 | |||
c00f05a1c1 | |||
db3ddf07ee | |||
cd16522805 | |||
5fec881387 | |||
3ac68e810d | |||
e6fe5c827c | |||
65e1e2cf4f | |||
e36a2c68f1 | |||
add9357257 | |||
ad3d915fc5 | |||
36f400d542 | |||
dd1a19745a | |||
58daedc89e | |||
d20a8fcf13 | |||
e56bf82c31 | |||
0f9895eec8 | |||
f776c36e70 | |||
1ef01b53f2 | |||
720169dce3 | |||
0d09039e5f | |||
cc56fde9fe | |||
3a0b3de175 | |||
47e544b710 | |||
44d6c4fe44 | |||
e5693ed668 | |||
8c21aa86e9 | |||
f7c5b42435 | |||
e3404cd3d3 | |||
8b57169e92 | |||
ab9a26f6bd | |||
8779b263ab | |||
3135db4bb2 | |||
734cb0be6e | |||
1f259f9298 | |||
427fbfdf5e | |||
0c860c0fe9 | |||
5b2a099203 | |||
ccadfc8fe5 | |||
3aead3a2a9 | |||
6a48fed170 | |||
ea1684133b | |||
e5263d0345 | |||
24e1b4034e | |||
dfa5c229b3 | |||
87be54aa4a | |||
82d9ae31bd | |||
e5518b7615 | |||
e5a22eafe7 | |||
7a52afd223 | |||
296201d6b7 | |||
162b639705 | |||
5dda32bb81 | |||
8ce8b60092 | |||
8ff2c01bf2 | |||
e22eebfd02 | |||
4fcdde4913 | |||
e41668862f | |||
a74a689c90 | |||
d85a76484c | |||
82bdf63419 | |||
9ce0bc6b5f | |||
bf524595e2 | |||
27c4db752c | |||
ca54984344 | |||
46aeab9a7a | |||
f365b53a0f | |||
d4dfa9a2c2 | |||
cf9e60fd92 | |||
21ae04d25d | |||
f1778ac5b4 | |||
ba10093ddc | |||
a5c9469698 | |||
75314c78e0 | |||
53edae1b6b | |||
356fc5b524 | |||
60150423d7 | |||
bcc42dd259 | |||
d59cb9c1e3 | |||
3006604922 | |||
1fbf8ca079 | |||
695813ef7d | |||
e3b70ca08d | |||
8857b7e0c1 | |||
4a7c20f5a0 | |||
29368fc953 | |||
0696e4bce0 | |||
255ed50685 | |||
00afee83b8 | |||
0d1bced122 | |||
46e734fc8e | |||
c39ae21f4a | |||
69aa13bc56 | |||
2c032ff70d | |||
0af4703b78 | |||
ea15bc782a | |||
9ec0f73e87 | |||
f9fb034330 | |||
6eb5a25ea1 | |||
45d8411f98 | |||
d9e2317e62 | |||
336221a972 | |||
dd998be1e7 | |||
3c3b09209c | |||
4a6571d310 | |||
cb67f1de52 | |||
402e2c47fb | |||
58b2895ec9 | |||
00b2853d3d | |||
634ceeec50 | |||
d7442d771b | |||
8f22480ec9 | |||
9d974273af | |||
3a8aa3e8cd | |||
9e67abcc8a | |||
d0bcd30909 | |||
b97aa23548 | |||
e6ca54fd04 | |||
4502902fb0 | |||
5f34539525 | |||
953f5fb025 | |||
4f3a0b3523 | |||
1d144e6767 | |||
056dbaefda | |||
3a15c6b843 | |||
db20d04c4b | |||
c5e8c9f01f | |||
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 |
.editorconfig
.github
.gitignore.idea
CODE_OF_CONDUCT.mdCONTRIBUTING.mdREADME.mdapp
.gitignorebuild.gradle.ktsproguard-android-optimize.txtproguard-rules.proshortcuts.xml
build.gradle.ktssrc
main
AndroidManifest.xmlbaseline-prof.txt
java
eu
kanade
core
preference
util
data
domain
DomainModule.kt
backup
service
base
category
chapter
interactor
model
download
interactor
extension
library
service
manga
interactor
model
source
interactor
model
service
track
interactor
model
service
store
ui
presentation
browse
BrowseBadges.ktBrowseSourceScreen.ktExtensionDetailsScreen.ktExtensionFilterScreen.ktExtensionsScreen.ktGlobalSearchScreen.ktMigrateMangaScreen.ktMigrateSearchScreen.ktMigrateSourceScreen.ktSourcesFilterScreen.ktSourcesScreen.kt
components
category
components
AdaptiveSheet.ktAlertDialog.ktAppBar.ktBadges.ktBanners.ktChangeCategoryDialog.ktDateText.ktDeleteLibraryMangaDialog.ktDivider.ktDownloadDropdownMenu.ktDropdownMenu.ktDuplicateMangaDialog.ktEmptyScreen.ktPager.ktPullRefresh.ktRelativeDateHeader.ktSettingsItems.ktTabbedDialog.ktTabbedScreen.ktTabs.kt
crash
history
library
manga
more
LogoHeader.ktMoreScreen.ktNewUpdateScreen.kt
onboarding
settings
Preference.ktPreferenceItem.ktPreferenceScaffold.ktPreferenceScreen.kt
screen
Commons.ktLicensesScreen.ktSearchableSettings.ktSettingsAdvancedScreen.ktSettingsAppearanceScreen.ktSettingsBackupScreen.ktSettingsBrowseScreen.ktSettingsDataScreen.ktSettingsDownloadScreen.ktSettingsGeneralScreen.ktSettingsLibraryScreen.ktSettingsMainScreen.ktSettingsReaderScreen.ktSettingsSearchScreen.ktSettingsSecurityScreen.ktSettingsTrackingScreen.kt
about
advanced
appearance
browse
data
debug
widget
stats
reader
ChapterTransition.ktDisplayRefreshHost.ktOrientationSelectDialog.ktPageIndicatorText.ktReaderContentOverlay.ktReaderPageActionsDialog.ktReadingModeSelectDialog.kt
appbars
components
settings
theme
TachiyomiTheme.kt
colorscheme
track
TrackInfoDialogHome.ktTrackInfoDialogHomePreviewProvider.ktTrackInfoDialogSelector.ktTrackerSearch.ktTrackerSearchPreviewProvider.kt
components
updates
util
ChapterNumberFormatter.ktExceptionFormatter.ktModifier.ktNavigator.ktPermissions.ktPreview.ktResources.ktScrollable.ktTimeUtils.kt
webview
tachiyomi
App.ktAppInfo.ktMigrations.kt
crash
data
backup
BackupConst.ktBackupCreatorJob.ktBackupDecoder.ktBackupFileValidator.ktBackupManager.ktBackupNotifier.ktBackupRestoreService.ktBackupRestorer.kt
create
models
Backup.ktBackupCategory.ktBackupChapter.ktBackupHistory.ktBackupManga.ktBackupPreference.ktBackupSerializer.ktBackupSource.ktBackupTracking.kt
restore
cache
coil
database
download
DownloadCache.ktDownloadJob.ktDownloadManager.ktDownloadNotifier.ktDownloadPendingDeleter.ktDownloadProvider.ktDownloadService.ktDownloadStore.ktDownloader.kt
model
library
notification
preference
saver
track
BaseTracker.ktDeletableTracker.ktEnhancedTracker.ktTrackManager.ktTrackService.ktTracker.ktTrackerManager.kt
anilist
bangumi
kavita
kitsu
komga
mangaupdates
model
myanimelist
shikimori
suwayomi
updater
di
extension
source
ui
base
browse
BrowseTab.kt
extension
ExtensionFilterScreen.ktExtensionFilterScreenModel.ktExtensionsScreenModel.ktExtensionsTab.kt
details
migration
MigrationFlags.kt
manga
search
MigrateDialog.ktMigrateSearchScreen.ktMigrateSearchScreenDialogScreenModel.ktMigrateSearchScreenModel.ktSourceSearchScreen.kt
sources
source
category
deeplink
download
history
home
library
LibraryItem.ktLibraryScreenModel.ktLibrarySettingsScreenModel.ktLibrarySettingsSheet.ktLibraryTab.kt
main
manga
more
reader
PageIndicatorTextView.ktReaderActivity.ktReaderColorFilterView.ktReaderNavigationOverlayView.ktReaderPageSheet.ktReaderSlider.ktReaderViewModel.ktSaveImageNotifier.kt
loader
ChapterLoader.ktDirectoryPageLoader.ktDownloadPageLoader.ktEpubPageLoader.ktHttpPageLoader.ktPageLoader.ktRarPageLoader.ktZipPageLoader.kt
model
setting
OrientationType.ktReaderColorFilterSettings.ktReaderGeneralSettings.ktReaderOrientation.ktReaderPreferences.ktReaderReadingModeSettings.ktReaderSettingsScreenModel.ktReaderSettingsSheet.ktReadingMode.ktReadingModeType.kt
viewer
security
setting
SettingsScreen.kt
track
stats
updates
webview
util
BackupUtil.ktCrashLogUtil.ktMangaExtensions.ktPkceUtil.kt
chapter
lang
preference
storage
system
ActivityExtensions.ktAnimationExtensions.ktAuthenticatorUtil.ktContextExtensions.ktDisplayExtensions.ktDrawableExtensions.ktGLUtil.ktIntentExtensions.ktLocaleHelper.ktNetworkExtensions.ktNetworkStateTracker.ktNotificationExtensions.ktWorkManagerExtensions.kt
view
widget
test
res
anim
bottom_sheet_slide_in.xmlbottom_sheet_slide_out.xmlenter_from_bottom.xmlenter_from_top.xmlexit_to_bottom.xmlexit_to_top.xml
color
drawable-v26
drawable
anim_caret_down.xmlempty_drawable_32dp.xmlic_arrow_down_white_32dp.xmlic_arrow_up_white_32dp.xmlic_blank_24dp.xmlic_book_24dp.xmlic_bookmark_24dp.xmlic_bookmark_border_24dp.xmlic_brightness_5_24dp.xmlic_check_24dp.xmlic_check_box_24dp.xmlic_check_box_outline_blank_24dp.xmlic_check_box_x_24dp.xmlic_close_24dp.xmlic_crop_24dp.xmlic_crop_off_24dp.xmlic_delete_24dp.xmlic_discord_24dp.xmlic_done_24dp.xmlic_done_prev_24dp.xmlic_drag_handle_24dp.xmlic_expand_less_24dp.xmlic_expand_more_24dp.xmlic_extension_24dp.xmlic_facebook_24dp.xmlic_folder_24dp.xmlic_github_24dp.xmlic_glasses_24dp.xmlic_info_24dp.xmlic_offline_pin_24dp.xmlic_overflow_24dp.xmlic_pause_24dp.xmlic_photo_24dp.xmlic_play_arrow_24dp.xmlic_reader_continuous_vertical_24dp.xmlic_reader_default_24dp.xmlic_reader_ltr_24dp.xmlic_reader_rtl_24dp.xmlic_reader_vertical_24dp.xmlic_reader_webtoon_24dp.xmlic_reddit_24dp.xmlic_refresh_24dp.xmlic_save_24dp.xmlic_screen_lock_landscape_24dp.xmlic_screen_lock_portrait_24dp.xmlic_screen_rotation_24dp.xmlic_settings_24dp.xmlic_share_24dp.xmlic_skip_next_24dp.xmlic_stay_current_landscape_24dp.xmlic_stay_current_portrait_24dp.xmlic_system_update_alt_white_24dp.xmlic_tachi.xmlic_twitter_24dp.xmlic_warning_white_24dp.xmlic_webview_24dp.xmllist_item_selector.xmlmaterial_thumb_drawable.xmlsc_collections_bookmark_48dp.xmlsc_explore_48dp.xmlsc_history_48dp.xmlsc_new_releases_48dp.xmltransparent_tabs_background.xml
layout
common_spinner_item.xmlcommon_tabbed_sheet.xmlcompose_controller.xmldownload_list.xmlmaterial_fastscroll.xmlnavigation_view_checkbox.xmlnavigation_view_checkedtext.xmlnavigation_view_group.xmlnavigation_view_radio.xmlnavigation_view_spinner.xmlnavigation_view_text.xmlpref_spinner.xmlreader_activity.xmlreader_color_filter_settings.xmlreader_general_settings.xmlreader_page_sheet.xmlreader_pager_settings.xmlreader_reading_mode_settings.xmlreader_transition_view.xmlreader_webtoon_settings.xmlsource_filter_sheet.xmlsource_preferences_controller.xml
menu
values-sw720dp
values-v28
values
xml
standard
buildSrc
core-metadata
core
build.gradle.kts
src
main
java
eu
kanade
tachiyomi
core
security
network
DohProviders.ktJavaScriptEngine.ktNetworkHelper.ktNetworkPreferences.ktOkHttpExtensions.ktProgressResponseBody.kt
interceptor
util
tachiyomi
core
data
build.gradle.kts
src
main
java
tachiyomi
data
AndroidDatabaseHandler.ktDatabaseAdapter.ktDatabaseHandler.ktQueryPagingSource.kt
category
chapter
history
manga
release
source
SourceDataRepositoryImpl.ktSourceMapper.ktSourcePagingSource.ktSourceRepositoryImpl.ktStubSourceRepositoryImpl.kt
track
updates
sqldelight
domain
build.gradle.kts
gradle.propertiessrc
main
java
tachiyomi
domain
backup
service
category
interactor
chapter
interactor
GetChapter.ktGetChapterByUrlAndMangaId.ktGetChaptersByMangaId.ktSetMangaDefaultChapterFlags.ktUpdateChapter.kt
model
repository
service
download
service
history
interactor
model
repository
library
manga
interactor
FetchInterval.ktGetDuplicateLibraryManga.ktGetFavorites.ktGetLibraryManga.ktGetManga.ktGetMangaByUrlAndSourceId.ktGetMangaWithChapters.ktNetworkToLocalManga.ktResetViewerFlags.ktSetMangaChapterFlags.kt
model
repository
release
source
interactor
model
repository
service
storage
track
interactor
model
repository
updates
test
java
tachiyomi
domain
chapter
library
model
manga
interactor
release
interactor
gradle
gradlewi18n
.gitignoreREADME.mdbuild.gradle.kts
src
androidMain
commonMain
resources
MR
am
ar
base
be
bg
bn
ca
ceb
cs
cv
da
de
el
eo
es
eu
fa
fi
fil
fr
gl
he
hi
hr
hu
in
it
ja
jv
ka-rGE
kk
km
kn
ko
lt
lv
mr
ms
nb-rNO
ne
nl
nn
pl
pt-rBR
pt
ro
ru
sa
sah
sc
sdh
sk
sq
sr
sv
te
th
tr
uk
uz
vi
zh-rCN
zh-rTW
main
res
values-b+es+419
values-ml
values-my
values-ne
values-or
values-si
values-ta
values-ti
values-ur-rPK
values-ur
macrobenchmark
presentation-core
build.gradle.kts
src
main
java
tachiyomi
presentation
core
components
ActionButton.ktAdaptiveSheet.ktBadges.ktCircularProgressIndicator.ktCollapsibleBox.ktLabeledCheckbox.ktLazyColumnWithAction.ktLazyGrid.ktLazyList.ktLinkIcon.ktListGroupHeader.ktPager.ktPill.ktSectionCard.ktSettingsItems.ktTwoPanelBox.ktVerticalFastScroller.ktWheelPicker.kt
material
i18n
icons
screens
theme
util
res
presentation-widget
settings.gradle.ktssource-api
build.gradle.kts
src
androidMain
commonMain
kotlin
main
java
eu
kanade
tachiyomi
source-local
.gitignorebuild.gradle.ktsconsumer-rules.proproguard-rules.pro
src
androidMain
commonMain
kotlin
tachiyomi
@ -1,4 +1,5 @@
|
||||
[*.{kt,kts}]
|
||||
max_line_length = 120
|
||||
indent_size = 4
|
||||
insert_final_newline = true
|
||||
ij_kotlin_allow_trailing_comma = true
|
||||
|
6
.github/ISSUE_TEMPLATE.md
vendored
6
.github/ISSUE_TEMPLATE.md
vendored
@ -3,10 +3,10 @@
|
||||
I acknowledge that:
|
||||
|
||||
- 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.15.3)
|
||||
- All extensions
|
||||
- I have tried the troubleshooting guide: https://tachiyomi.org/help/guides/troubleshooting-problems/
|
||||
- If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions
|
||||
- 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 official extension, that I should be opening an issue in https://github.com/tachiyomiorg/extensions
|
||||
- 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
|
||||
|
||||
|
8
.github/ISSUE_TEMPLATE/config.yml
vendored
8
.github/ISSUE_TEMPLATE/config.yml
vendored
@ -1,11 +1,11 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: ⚠️ Extension/source issue
|
||||
url: https://github.com/tachiyomiorg/tachiyomi-extensions/issues/new/choose
|
||||
about: Issues and requests for extensions and sources should be opened in the tachiyomi-extensions repository instead
|
||||
url: https://github.com/tachiyomiorg/extensions/issues/new/choose
|
||||
about: Issues and requests for official extensions and sources should be opened in the extensions repository instead
|
||||
- name: 📦 Tachiyomi extensions
|
||||
url: https://tachiyomi.org/extensions
|
||||
url: https://tachiyomi.org/extensions/
|
||||
about: List of all available extensions with download links
|
||||
- name: 🖥️ Tachiyomi website
|
||||
url: https://tachiyomi.org/help/
|
||||
url: https://tachiyomi.org/
|
||||
about: Guides, troubleshooting, and answers to common questions
|
||||
|
8
.github/ISSUE_TEMPLATE/report_issue.yml
vendored
8
.github/ISSUE_TEMPLATE/report_issue.yml
vendored
@ -53,7 +53,7 @@ body:
|
||||
label: Tachiyomi version
|
||||
description: You can find your Tachiyomi version in **More → About**.
|
||||
placeholder: |
|
||||
Example: "0.14.4"
|
||||
Example: "0.15.3"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@ -94,11 +94,11 @@ body:
|
||||
required: true
|
||||
- label: I have written a short but informative title.
|
||||
required: true
|
||||
- label: If this is an issue with an extension, I should be opening an issue in the [extensions repository](https://github.com/tachiyomiorg/tachiyomi-extensions/issues/new/choose).
|
||||
- label: If this is an issue with an official extension, I should be opening an issue in the [extensions repository](https://github.com/tachiyomiorg/extensions/issues/new/choose).
|
||||
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
|
||||
- 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.15.3](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**.
|
||||
required: true
|
||||
- label: I have updated all installed extensions.
|
||||
required: true
|
||||
|
4
.github/ISSUE_TEMPLATE/request_feature.yml
vendored
4
.github/ISSUE_TEMPLATE/request_feature.yml
vendored
@ -31,9 +31,9 @@ body:
|
||||
required: true
|
||||
- label: I have written a short but informative title.
|
||||
required: true
|
||||
- label: If this is an issue with an extension, I should be opening an issue in the [extensions repository](https://github.com/tachiyomiorg/tachiyomi-extensions/issues/new/choose).
|
||||
- label: If this is an issue with an official extension, I should be opening an issue in the [extensions repository](https://github.com/tachiyomiorg/extensions/issues/new/choose).
|
||||
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.15.3](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**.
|
||||
required: true
|
||||
- label: I will fill out all of the requested information in this form.
|
||||
required: true
|
||||
|
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",
|
||||
],
|
||||
}
|
||||
]
|
||||
}
|
13
.github/workflows/build_pull_request.yml
vendored
13
.github/workflows/build_pull_request.yml
vendored
@ -3,7 +3,8 @@ on:
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
- 'i18n/src/main/res/**/strings.xml'
|
||||
- 'i18n/src/commonMain/resources/**/strings.xml'
|
||||
- 'i18n/src/commonMain/resources/**/plurals.xml'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
|
||||
@ -19,7 +20,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Clone repo
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Validate Gradle Wrapper
|
||||
uses: gradle/wrapper-validation-action@v1
|
||||
@ -27,13 +28,13 @@ jobs:
|
||||
- name: Dependency Review
|
||||
uses: actions/dependency-review-action@v3
|
||||
|
||||
- name: Set up JDK 11
|
||||
uses: actions/setup-java@v3
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: 11
|
||||
java-version: 17
|
||||
distribution: adopt
|
||||
|
||||
- name: Build app and run unit tests
|
||||
uses: gradle/gradle-command-action@v2
|
||||
with:
|
||||
arguments: lintKotlin assembleStandardRelease testStandardReleaseUnitTest
|
||||
arguments: ktlintCheck assembleStandardRelease testReleaseUnitTest
|
20
.github/workflows/build_push.yml
vendored
20
.github/workflows/build_push.yml
vendored
@ -17,21 +17,21 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Clone repo
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Validate Gradle Wrapper
|
||||
uses: gradle/wrapper-validation-action@v1
|
||||
|
||||
- name: Set up JDK 11
|
||||
uses: actions/setup-java@v3
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: 11
|
||||
java-version: 17
|
||||
distribution: adopt
|
||||
|
||||
- name: Build app and run unit tests
|
||||
uses: gradle/gradle-command-action@v2
|
||||
with:
|
||||
arguments: lintKotlin assembleStandardRelease testStandardReleaseUnitTest
|
||||
arguments: ktlintCheck assembleStandardRelease testReleaseUnitTest
|
||||
|
||||
# Sign APK and create release for tags
|
||||
|
||||
@ -104,3 +104,13 @@ jobs:
|
||||
prerelease: false
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
update-website:
|
||||
needs: [build]
|
||||
runs-on: ubuntu-latest
|
||||
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'tachiyomiorg/tachiyomi'
|
||||
steps:
|
||||
- name: Trigger Netlify build hook
|
||||
run: curl -s -X POST -d {} "https://api.netlify.com/build_hooks/${TOKEN}"
|
||||
env:
|
||||
TOKEN: ${{ secrets.NETLIFY_HOOK_RELEASE }}
|
||||
|
12
.github/workflows/issue_moderator.yml
vendored
12
.github/workflows/issue_moderator.yml
vendored
@ -11,9 +11,11 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Moderate issues
|
||||
uses: tachiyomiorg/issue-moderator-action@v1
|
||||
uses: tachiyomiorg/issue-moderator-action@v2
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
duplicate-label: Duplicate
|
||||
|
||||
auto-close-rules: |
|
||||
[
|
||||
{
|
||||
@ -31,5 +33,13 @@ jobs:
|
||||
"regex": "^(?!.*myanimelist.*).*(aniyomi|anime).*$",
|
||||
"ignoreCase": true,
|
||||
"message": "Tachiyomi does not support anime, and has no plans to support anime. In addition Tachiyomi is not affiliated with Aniyomi https://github.com/jmir1/aniyomi"
|
||||
},
|
||||
{
|
||||
"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
|
||||
|
2
.github/workflows/lock.yml
vendored
2
.github/workflows/lock.yml
vendored
@ -12,7 +12,7 @@ jobs:
|
||||
lock:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/lock-threads@v4
|
||||
- uses: dessant/lock-threads@v5
|
||||
with:
|
||||
github-token: ${{ github.token }}
|
||||
issue-inactive-days: '2'
|
||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -2,7 +2,8 @@
|
||||
/local.properties
|
||||
/.idea/workspace.xml
|
||||
.DS_Store
|
||||
.idea/
|
||||
.idea/*
|
||||
!.idea/icon.png
|
||||
*iml
|
||||
*.iml
|
||||
|
||||
|
BIN
.idea/icon.png
generated
Normal file
BIN
.idea/icon.png
generated
Normal file
Binary file not shown.
After ![]() (image error) Size: 14 KiB |
@ -59,8 +59,7 @@ representative at an online or offline event.
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community moderators responsible for enforcement at
|
||||
the [Tachiyomi Discord server](https://discord.gg/tachiyomi).
|
||||
reported to the community moderators via issues.
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community moderators are obligated to respect the privacy and security of the
|
||||
|
@ -24,13 +24,17 @@ Before you start, please note that the ability to use following technologies is
|
||||
- [Android Studio](https://developer.android.com/studio)
|
||||
- Emulator or phone with developer options enabled to test changes.
|
||||
|
||||
## Linting
|
||||
|
||||
To auto-fix some linting errors, run the `ktlintFormat` Gradle task.
|
||||
|
||||
## Getting help
|
||||
|
||||
- Join [the Discord server](https://discord.gg/tachiyomi) for online help and to ask questions while developing.
|
||||
No support is currently provided.
|
||||
|
||||
# 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
|
||||
|
15
README.md
15
README.md
@ -1,7 +1,6 @@
|
||||
| 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) |
|
||||
|
||||
| Build | Stable | Weekly Preview | Contribute |
|
||||
|-------|--------|----------------|------------|
|
||||
| [](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) |
|
||||
|
||||
# Tachiyomi
|
||||
Tachiyomi is a free and open source manga reader for Android 6.0 and above.
|
||||
@ -29,8 +28,7 @@ Please make sure to read the full guidelines. Your issue may be closed without w
|
||||
|
||||
<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).**
|
||||
2. If you are unsure, ask here: [](https://discord.gg/tachiyomi)
|
||||
**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).**
|
||||
|
||||
</details>
|
||||
|
||||
@ -38,7 +36,7 @@ Please make sure to read the full guidelines. Your issue may be closed without w
|
||||
|
||||
* Include version (More → About → Version)
|
||||
* If not latest, try updating, it may have already been solved
|
||||
* Preview version is equal to the number of commits as seen in the main page
|
||||
* 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 screenshot (if needed)
|
||||
* If it could be device-dependent, try reproducing on another device (if possible)
|
||||
@ -55,7 +53,7 @@ DON'T: https://github.com/tachiyomiorg/tachiyomi/issues/75
|
||||
* Write a detailed issue, explaining what it should do or how. Avoid writing just "like X app does"
|
||||
* Include screenshot (if needed)
|
||||
|
||||
Source requests should be created at https://github.com/tachiyomiorg/tachiyomi-extensions, they do not belong in this repository.
|
||||
Source requests are not accepted.
|
||||
</details>
|
||||
|
||||
<details><summary>Contributing</summary>
|
||||
@ -71,7 +69,6 @@ See [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md).
|
||||
## FAQ
|
||||
|
||||
[See our website.](https://tachiyomi.org/)
|
||||
You can also reach out to us on [Discord](https://discord.gg/tachiyomi).
|
||||
|
||||
## License
|
||||
|
||||
|
1
app/.gitignore
vendored
1
app/.gitignore
vendored
@ -1,4 +1,3 @@
|
||||
/build
|
||||
*iml
|
||||
*.iml
|
||||
custom.gradle
|
||||
|
@ -1,5 +1,4 @@
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
import org.jmailen.gradle.kotlinter.tasks.LintTask
|
||||
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
@ -9,10 +8,6 @@ plugins {
|
||||
id("com.github.zellius.shortcut-helper")
|
||||
}
|
||||
|
||||
if (gradle.startParameter.taskRequests.toString().contains("Standard")) {
|
||||
apply<com.google.gms.googleservices.GoogleServicesPlugin>()
|
||||
}
|
||||
|
||||
shortcutHelper.setFilePath("./shortcuts.xml")
|
||||
|
||||
val SUPPORTED_ABIS = setOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
|
||||
@ -22,8 +17,9 @@ android {
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "eu.kanade.tachiyomi"
|
||||
versionCode = 95
|
||||
versionName = "0.14.4"
|
||||
|
||||
versionCode = 119
|
||||
versionName = "0.15.3"
|
||||
|
||||
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
||||
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
|
||||
@ -31,9 +27,6 @@ android {
|
||||
buildConfigField("boolean", "INCLUDE_UPDATER", "false")
|
||||
buildConfigField("boolean", "PREVIEW", "false")
|
||||
|
||||
// Please disable ACRA or use your own instance in forked versions of the project
|
||||
buildConfigField("String", "ACRA_URI", "\"https://tachiyomi.kanade.eu/crash_report\"")
|
||||
|
||||
ndk {
|
||||
abiFilters += SUPPORTED_ABIS
|
||||
}
|
||||
@ -65,11 +58,11 @@ android {
|
||||
initWith(getByName("release"))
|
||||
buildConfigField("boolean", "PREVIEW", "true")
|
||||
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
matchingFallbacks.add("release")
|
||||
val debugType = getByName("debug")
|
||||
signingConfig = debugType.signingConfig
|
||||
versionNameSuffix = debugType.versionNameSuffix
|
||||
applicationIdSuffix = debugType.applicationIdSuffix
|
||||
matchingFallbacks.add("release")
|
||||
}
|
||||
create("benchmark") {
|
||||
initWith(getByName("release"))
|
||||
@ -77,6 +70,7 @@ android {
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
matchingFallbacks.add("release")
|
||||
isDebuggable = false
|
||||
isProfileable = true
|
||||
versionNameSuffix = "-benchmark"
|
||||
applicationIdSuffix = ".benchmark"
|
||||
}
|
||||
@ -101,8 +95,9 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
resources.excludes.addAll(listOf(
|
||||
packaging {
|
||||
resources.excludes.addAll(
|
||||
listOf(
|
||||
"META-INF/DEPENDENCIES",
|
||||
"LICENSE.txt",
|
||||
"META-INF/LICENSE",
|
||||
@ -110,7 +105,8 @@ android {
|
||||
"META-INF/README.md",
|
||||
"META-INF/NOTICE",
|
||||
"META-INF/*.kotlin_module",
|
||||
))
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
dependenciesInfo {
|
||||
@ -120,6 +116,7 @@ android {
|
||||
buildFeatures {
|
||||
viewBinding = true
|
||||
compose = true
|
||||
buildConfig = true
|
||||
|
||||
// Disable some unused things
|
||||
aidl = false
|
||||
@ -140,7 +137,9 @@ android {
|
||||
dependencies {
|
||||
implementation(project(":i18n"))
|
||||
implementation(project(":core"))
|
||||
implementation(project(":core-metadata"))
|
||||
implementation(project(":source-api"))
|
||||
implementation(project(":source-local"))
|
||||
implementation(project(":data"))
|
||||
implementation(project(":domain"))
|
||||
implementation(project(":presentation-core"))
|
||||
@ -155,13 +154,12 @@ dependencies {
|
||||
implementation(compose.material.icons)
|
||||
implementation(compose.animation)
|
||||
implementation(compose.animation.graphics)
|
||||
implementation(compose.ui.tooling)
|
||||
debugImplementation(compose.ui.tooling)
|
||||
implementation(compose.ui.tooling.preview)
|
||||
implementation(compose.ui.util)
|
||||
implementation(compose.accompanist.webview)
|
||||
implementation(compose.accompanist.flowlayout)
|
||||
implementation(compose.accompanist.permissions)
|
||||
implementation(compose.accompanist.themeadapter)
|
||||
implementation(compose.accompanist.systemuicontroller)
|
||||
lintChecks(compose.lintchecks)
|
||||
|
||||
implementation(androidx.paging.runtime)
|
||||
implementation(androidx.paging.compose)
|
||||
@ -169,6 +167,7 @@ dependencies {
|
||||
implementation(libs.bundles.sqlite)
|
||||
|
||||
implementation(kotlinx.reflect)
|
||||
implementation(kotlinx.immutables)
|
||||
|
||||
implementation(platform(kotlinx.coroutines.bom))
|
||||
implementation(kotlinx.bundles.coroutines)
|
||||
@ -178,7 +177,6 @@ dependencies {
|
||||
implementation(androidx.appcompat)
|
||||
implementation(androidx.biometricktx)
|
||||
implementation(androidx.constraintlayout)
|
||||
implementation(androidx.coordinatorlayout)
|
||||
implementation(androidx.corektx)
|
||||
implementation(androidx.splashscreen)
|
||||
implementation(androidx.recyclerview)
|
||||
@ -188,20 +186,17 @@ dependencies {
|
||||
implementation(androidx.bundles.lifecycle)
|
||||
|
||||
// Job scheduling
|
||||
implementation(androidx.bundles.workmanager)
|
||||
implementation(androidx.workmanager)
|
||||
|
||||
// RX
|
||||
implementation(libs.bundles.reactivex)
|
||||
implementation(libs.flowreactivenetwork)
|
||||
// RxJava
|
||||
implementation(libs.rxjava)
|
||||
|
||||
// Network client
|
||||
// Networking
|
||||
implementation(libs.bundles.okhttp)
|
||||
implementation(libs.okio)
|
||||
implementation(libs.conscrypt.android) // TLS 1.3 support for Android < 10
|
||||
|
||||
// TLS 1.3 support for Android < 10
|
||||
implementation(libs.conscrypt.android)
|
||||
|
||||
// Data serialization (JSON, protobuf)
|
||||
// Data serialization (JSON, protobuf, xml)
|
||||
implementation(kotlinx.bundles.serialization)
|
||||
|
||||
// HTML parser
|
||||
@ -219,19 +214,16 @@ dependencies {
|
||||
implementation(libs.injekt.core)
|
||||
|
||||
// Image loading
|
||||
implementation(platform(libs.coil.bom))
|
||||
implementation(libs.bundles.coil)
|
||||
implementation(libs.subsamplingscaleimageview) {
|
||||
exclude(module = "image-decoder")
|
||||
}
|
||||
implementation(libs.image.decoder)
|
||||
|
||||
// Sort
|
||||
implementation(libs.natural.comparator)
|
||||
|
||||
// UI libraries
|
||||
implementation(libs.material)
|
||||
implementation(libs.flexible.adapter.core)
|
||||
implementation(libs.flexible.adapter.ui)
|
||||
implementation(libs.photoview)
|
||||
implementation(libs.directionalviewpager) {
|
||||
exclude(group = "androidx.viewpager", module = "viewpager")
|
||||
@ -239,23 +231,18 @@ dependencies {
|
||||
implementation(libs.insetter)
|
||||
implementation(libs.bundles.richtext)
|
||||
implementation(libs.aboutLibraries.compose)
|
||||
implementation(libs.cascade)
|
||||
implementation(libs.bundles.voyager)
|
||||
implementation(libs.wheelpicker)
|
||||
implementation(libs.materialmotion.core)
|
||||
implementation(libs.compose.materialmotion)
|
||||
implementation(libs.swipe)
|
||||
|
||||
// Logging
|
||||
implementation(libs.logcat)
|
||||
|
||||
// Crash reports/analytics
|
||||
implementation(libs.acra.http)
|
||||
"standardImplementation"(libs.firebase.analytics)
|
||||
|
||||
// Shizuku
|
||||
implementation(libs.bundles.shizuku)
|
||||
|
||||
// Tests
|
||||
testImplementation(libs.junit)
|
||||
testImplementation(libs.bundles.test)
|
||||
|
||||
// For detecting memory leaks; see https://square.github.io/leakcanary/
|
||||
// debugImplementation(libs.leakcanary.android)
|
||||
@ -266,7 +253,9 @@ androidComponents {
|
||||
beforeVariants { variantBuilder ->
|
||||
// Disables standardBenchmark
|
||||
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")) {
|
||||
@ -277,16 +266,10 @@ androidComponents {
|
||||
}
|
||||
|
||||
tasks {
|
||||
|
||||
withType<LintTask>().configureEach {
|
||||
exclude { it.file.path.contains("generated[\\\\/]".toRegex()) }
|
||||
}
|
||||
|
||||
// See https://kotlinlang.org/docs/reference/experimental.html#experimental-status-of-experimental-api(-markers)
|
||||
withType<KotlinCompile> {
|
||||
kotlinOptions.freeCompilerArgs += listOf(
|
||||
"-opt-in=coil.annotation.ExperimentalCoilApi",
|
||||
"-opt-in=com.google.accompanist.permissions.ExperimentalPermissionsApi",
|
||||
"-Xcontext-receivers",
|
||||
"-opt-in=androidx.compose.foundation.layout.ExperimentalLayoutApi",
|
||||
"-opt-in=androidx.compose.material.ExperimentalMaterialApi",
|
||||
"-opt-in=androidx.compose.material3.ExperimentalMaterial3Api",
|
||||
@ -295,6 +278,8 @@ tasks {
|
||||
"-opt-in=androidx.compose.foundation.ExperimentalFoundationApi",
|
||||
"-opt-in=androidx.compose.animation.ExperimentalAnimationApi",
|
||||
"-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.FlowPreview",
|
||||
"-opt-in=kotlinx.coroutines.InternalCoroutinesApi",
|
||||
@ -305,12 +290,12 @@ tasks {
|
||||
kotlinOptions.freeCompilerArgs += listOf(
|
||||
"-P",
|
||||
"plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=" +
|
||||
project.buildDir.absolutePath + "/compose_metrics"
|
||||
project.layout.buildDirectory.dir("compose_metrics").get().asFile.absolutePath,
|
||||
)
|
||||
kotlinOptions.freeCompilerArgs += listOf(
|
||||
"-P",
|
||||
"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
|
||||
-ignorewarnings
|
||||
-verbose
|
||||
|
||||
-keepattributes *Annotation*
|
||||
|
8
app/proguard-rules.pro
vendored
8
app/proguard-rules.pro
vendored
@ -1,14 +1,18 @@
|
||||
-dontobfuscate
|
||||
|
||||
-keep,allowoptimization class eu.kanade.**
|
||||
-keep,allowoptimization class tachiyomi.**
|
||||
|
||||
# Keep common dependencies used in extensions
|
||||
-keep,allowoptimization class androidx.preference.** { public protected *; }
|
||||
-keep,allowoptimization class kotlin.** { public protected *; }
|
||||
-keep,allowoptimization class kotlinx.coroutines.** { public protected *; }
|
||||
-keep,allowoptimization class kotlinx.serialization.** { public protected *; }
|
||||
-keep,allowoptimization class kotlin.time.** { public protected *; }
|
||||
-keep,allowoptimization class okhttp3.** { public protected *; }
|
||||
-keep,allowoptimization class okio.** { public protected *; }
|
||||
-keep,allowoptimization class rx.** { public protected *; }
|
||||
-keep,allowoptimization class org.jsoup.** { public protected *; }
|
||||
-keep,allowoptimization class rx.** { public protected *; }
|
||||
-keep,allowoptimization class app.cash.quickjs.** { public protected *; }
|
||||
-keep,allowoptimization class uy.kohesive.injekt.** { public protected *; }
|
||||
|
||||
@ -41,7 +45,7 @@
|
||||
|
||||
##---------------Begin: proguard configuration for kotlinx.serialization ----------
|
||||
-keepattributes *Annotation*, InnerClasses
|
||||
-dontnote kotlinx.serialization.AnnotationsKt # core serialization annotations
|
||||
-dontnote kotlinx.serialization.** # core serialization annotations
|
||||
|
||||
# kotlinx-serialization-json specific. Add this if you have java.lang.NoClassDefFoundError kotlinx.serialization.json.JsonObjectSerializer
|
||||
-keepclassmembers class kotlinx.serialization.json.** {
|
||||
|
@ -1,5 +1,4 @@
|
||||
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<shortcut
|
||||
android:enabled="true"
|
||||
android:icon="@drawable/sc_collections_bookmark_48dp"
|
||||
|
@ -8,7 +8,9 @@
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||
|
||||
<!-- Storage -->
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission
|
||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
tools:ignore="ScopedStorage" />
|
||||
|
||||
<!-- For background jobs -->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
@ -20,43 +22,82 @@
|
||||
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
|
||||
<uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" />
|
||||
<!-- To view extension packages in API 30+ -->
|
||||
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
|
||||
<uses-permission
|
||||
android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||
tools:ignore="QueryAllPackagesPermission" />
|
||||
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.READ_APP_SPECIFIC_LOCALES" />
|
||||
<uses-permission
|
||||
android:name="android.permission.READ_APP_SPECIFIC_LOCALES"
|
||||
tools:ignore="ProtectedPermissions" />
|
||||
|
||||
<!-- Remove permission from Firebase dependency -->
|
||||
<uses-permission android:name="com.google.android.gms.permission.AD_ID"
|
||||
tools:node="remove" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||
|
||||
<application
|
||||
android:name=".App"
|
||||
android:allowBackup="false"
|
||||
android:enableOnBackInvokedCallback="true"
|
||||
android:hardwareAccelerated="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:largeHeap="true"
|
||||
android:localeConfig="@xml/locales_config"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
android:preserveLegacyExternalStorage="true"
|
||||
android:requestLegacyExternalStorage="true"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:theme="@style/Theme.Tachiyomi"
|
||||
android:supportsRtl="true"
|
||||
android:networkSecurityConfig="@xml/network_security_config">
|
||||
|
||||
<!-- enable profiling by macrobenchmark -->
|
||||
<profileable
|
||||
android:shell="true"
|
||||
tools:targetApi="q" />
|
||||
android:theme="@style/Theme.Tachiyomi">
|
||||
|
||||
<activity
|
||||
android:name=".ui.main.MainActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTop"
|
||||
android:theme="@style/Theme.Tachiyomi.SplashScreen"
|
||||
android:exported="true">
|
||||
android:theme="@style/Theme.Tachiyomi.SplashScreen">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- Deep link to add repos -->
|
||||
<intent-filter android:label="@string/action_add_repo">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="tachiyomi" />
|
||||
<data android:host="add-repo" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- Open backup files -->
|
||||
<intent-filter android:label="@string/pref_restore_backup">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="file" />
|
||||
<data android:scheme="content" />
|
||||
<data android:host="*" />
|
||||
<data android:mimeType="*/*" />
|
||||
<!--
|
||||
Work around Android's ugly primitive PatternMatcher
|
||||
implementation that can't cope with finding a . early in
|
||||
the path unless it's explicitly matched.
|
||||
|
||||
See https://stackoverflow.com/a/31028507
|
||||
-->
|
||||
<data android:pathPattern=".*\\.tachibk" />
|
||||
<data android:pathPattern=".*\\..*\\.tachibk" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\.tachibk" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\.tachibk" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\.tachibk" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\.tachibk" />
|
||||
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\.tachibk" />
|
||||
</intent-filter>
|
||||
|
||||
<!--suppress AndroidDomInspection -->
|
||||
<meta-data
|
||||
android:name="android.app.shortcuts"
|
||||
@ -64,16 +105,16 @@
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:process=":error_handler"
|
||||
android:name=".crash.CrashActivity"
|
||||
android:exported="false" />
|
||||
android:exported="false"
|
||||
android:process=":error_handler" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.main.DeepLinkActivity"
|
||||
android:name=".ui.deeplink.DeepLinkActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/action_search"
|
||||
android:launchMode="singleTask"
|
||||
android:theme="@android:style/Theme.NoDisplay"
|
||||
android:label="@string/action_global_search"
|
||||
android:exported="true">
|
||||
android:theme="@android:style/Theme.NoDisplay">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEARCH" />
|
||||
<action android:name="com.google.android.gms.actions.SEARCH_ACTION" />
|
||||
@ -97,20 +138,21 @@
|
||||
|
||||
<activity
|
||||
android:name=".ui.reader.ReaderActivity"
|
||||
android:launchMode="singleTask"
|
||||
android:exported="false">
|
||||
android:exported="false"
|
||||
android:launchMode="singleTask">
|
||||
<intent-filter>
|
||||
<action android:name="com.samsung.android.support.REMOTE_ACTION" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data android:name="com.samsung.android.support.REMOTE_ACTION"
|
||||
<meta-data
|
||||
android:name="com.samsung.android.support.REMOTE_ACTION"
|
||||
android:resource="@xml/s_pen_actions" />
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".ui.security.UnlockActivity"
|
||||
android:theme="@style/Theme.Tachiyomi"
|
||||
android:exported="false" />
|
||||
android:exported="false"
|
||||
android:theme="@style/Theme.Tachiyomi" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.webview.WebViewActivity"
|
||||
@ -119,67 +161,25 @@
|
||||
|
||||
<activity
|
||||
android:name=".extension.util.ExtensionInstallActivity"
|
||||
android:theme="@android:style/Theme.Translucent.NoTitleBar"
|
||||
android:exported="false" />
|
||||
android:exported="false"
|
||||
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.setting.track.AnilistLoginActivity"
|
||||
android:label="Anilist"
|
||||
android:exported="true">
|
||||
android:name=".ui.setting.track.TrackLoginActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/track_activity_name">
|
||||
<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="anilist-auth"
|
||||
android:scheme="tachiyomi" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ui.setting.track.MyAnimeListLoginActivity"
|
||||
android:label="MyAnimeList"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<data android:scheme="tachiyomi" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<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" />
|
||||
<data android:host="anilist-auth" />
|
||||
<data android:host="bangumi-auth" />
|
||||
<data android:host="myanimelist-auth" />
|
||||
<data android:host="shikimori-auth" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
@ -187,38 +187,10 @@
|
||||
android:name=".data.notification.NotificationReceiver"
|
||||
android:exported="false" />
|
||||
|
||||
<receiver
|
||||
android:name="tachiyomi.presentation.widget.UpdatesGridGlanceReceiver"
|
||||
android:enabled="@bool/glance_appwidget_available"
|
||||
<service
|
||||
android:name=".extension.util.ExtensionInstallService"
|
||||
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
|
||||
android:name=".data.download.DownloadService"
|
||||
android:exported="false" />
|
||||
|
||||
<service
|
||||
android:name=".data.updater.AppUpdateService"
|
||||
android:exported="false" />
|
||||
|
||||
<service
|
||||
android:name=".data.backup.BackupRestoreService"
|
||||
android:exported="false" />
|
||||
|
||||
<service android:name=".extension.util.ExtensionInstallService"
|
||||
android:exported="false" />
|
||||
android:foregroundServiceType="shortService" />
|
||||
|
||||
<service
|
||||
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
|
||||
@ -229,6 +201,11 @@
|
||||
android:value="true" />
|
||||
</service>
|
||||
|
||||
<service
|
||||
android:name="androidx.work.impl.foreground.SystemForegroundService"
|
||||
android:foregroundServiceType="dataSync"
|
||||
tools:node="merge" />
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.provider"
|
||||
@ -242,9 +219,9 @@
|
||||
<provider
|
||||
android:name="rikka.shizuku.ShizukuProvider"
|
||||
android:authorities="${applicationId}.shizuku"
|
||||
android:multiprocess="false"
|
||||
android:enabled="true"
|
||||
android:exported="true"
|
||||
android:multiprocess="false"
|
||||
android:permission="android.permission.INTERACT_ACROSS_USERS_FULL" />
|
||||
|
||||
<meta-data
|
||||
@ -254,11 +231,6 @@
|
||||
android:name="android.webkit.WebView.MetricsOptOut"
|
||||
android:value="true" />
|
||||
|
||||
<!-- Disable advertising ID collection for Firebase -->
|
||||
<meta-data
|
||||
android:name="google_analytics_adid_collection_enabled"
|
||||
android:value="false" />
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
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.mutableStateOf
|
||||
@ -31,7 +31,7 @@ class PreferenceMutableState<T>(
|
||||
}
|
||||
|
||||
override fun component2(): (T) -> Unit {
|
||||
return { preference.set(it) }
|
||||
return preference::set
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
package eu.kanade.core.util
|
||||
|
||||
import androidx.compose.ui.util.fastForEach
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import kotlin.contracts.ExperimentalContracts
|
||||
import kotlin.contracts.contract
|
||||
|
||||
@ -12,23 +11,14 @@ fun <T : R, R : Any> List<T>.insertSeparators(
|
||||
val newList = mutableListOf<R>()
|
||||
for (i in -1..lastIndex) {
|
||||
val before = getOrNull(i)
|
||||
before?.let { newList.add(it) }
|
||||
before?.let(newList::add)
|
||||
val after = getOrNull(i + 1)
|
||||
val separator = generator.invoke(before, after)
|
||||
separator?.let { newList.add(it) }
|
||||
separator?.let(newList::add)
|
||||
}
|
||||
return newList
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new map containing only the key entries of [transform] that are not null.
|
||||
*/
|
||||
inline fun <K, V, R> Map<out K, V>.mapNotNullKeys(transform: (Map.Entry<K?, V>) -> R?): ConcurrentHashMap<R, V> {
|
||||
val mutableMap = ConcurrentHashMap<R, V>()
|
||||
forEach { element -> transform(element)?.let { mutableMap[it] = element.value } }
|
||||
return mutableMap
|
||||
}
|
||||
|
||||
fun <E> HashSet<E>.addOrRemove(value: E, shouldAdd: Boolean) {
|
||||
if (shouldAdd) {
|
||||
add(value)
|
||||
@ -80,7 +70,7 @@ inline fun <T, R> List<T>.fastMapNotNull(transform: (T) -> R?): List<R> {
|
||||
contract { callsInPlace(transform) }
|
||||
val destination = ArrayList<R>()
|
||||
fastForEach { element ->
|
||||
transform(element)?.let { destination.add(it) }
|
||||
transform(element)?.let(destination::add)
|
||||
}
|
||||
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,85 @@
|
||||
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.GetAvailableScanlators
|
||||
import eu.kanade.domain.chapter.interactor.SetReadStatus
|
||||
import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource
|
||||
import eu.kanade.domain.chapter.interactor.SyncChaptersWithTrackServiceTwoWay
|
||||
import eu.kanade.domain.chapter.interactor.UpdateChapter
|
||||
import eu.kanade.domain.download.interactor.DeleteDownload
|
||||
import eu.kanade.domain.extension.interactor.CreateExtensionRepo
|
||||
import eu.kanade.domain.extension.interactor.DeleteExtensionRepo
|
||||
import eu.kanade.domain.extension.interactor.GetExtensionLanguages
|
||||
import eu.kanade.domain.extension.interactor.GetExtensionRepos
|
||||
import eu.kanade.domain.extension.interactor.GetExtensionSources
|
||||
import eu.kanade.domain.extension.interactor.GetExtensionsByType
|
||||
import eu.kanade.domain.history.interactor.GetNextChapters
|
||||
import eu.kanade.domain.manga.interactor.GetDuplicateLibraryManga
|
||||
import eu.kanade.domain.manga.interactor.GetFavorites
|
||||
import eu.kanade.domain.manga.interactor.GetLibraryManga
|
||||
import eu.kanade.domain.manga.interactor.GetManga
|
||||
import eu.kanade.domain.manga.interactor.GetMangaWithChapters
|
||||
import eu.kanade.domain.manga.interactor.NetworkToLocalManga
|
||||
import eu.kanade.domain.manga.interactor.ResetViewerFlags
|
||||
import eu.kanade.domain.manga.interactor.SetMangaChapterFlags
|
||||
import eu.kanade.domain.extension.interactor.TrustExtension
|
||||
import eu.kanade.domain.manga.interactor.GetExcludedScanlators
|
||||
import eu.kanade.domain.manga.interactor.SetExcludedScanlators
|
||||
import eu.kanade.domain.manga.interactor.SetMangaViewerFlags
|
||||
import eu.kanade.domain.manga.interactor.UpdateManga
|
||||
import eu.kanade.domain.source.interactor.GetEnabledSources
|
||||
import eu.kanade.domain.source.interactor.GetLanguagesWithSources
|
||||
import eu.kanade.domain.source.interactor.GetRemoteManga
|
||||
import eu.kanade.domain.source.interactor.GetSourcesWithFavoriteCount
|
||||
import eu.kanade.domain.source.interactor.GetSourcesWithNonLibraryManga
|
||||
import eu.kanade.domain.source.interactor.SetMigrateSorting
|
||||
import eu.kanade.domain.source.interactor.ToggleLanguage
|
||||
import eu.kanade.domain.source.interactor.ToggleSource
|
||||
import eu.kanade.domain.source.interactor.ToggleSourcePin
|
||||
import eu.kanade.domain.source.repository.SourceRepository
|
||||
import eu.kanade.domain.track.interactor.DeleteTrack
|
||||
import eu.kanade.domain.track.interactor.GetTracks
|
||||
import eu.kanade.domain.track.interactor.GetTracksPerManga
|
||||
import eu.kanade.domain.track.interactor.InsertTrack
|
||||
import eu.kanade.domain.track.interactor.AddTracks
|
||||
import eu.kanade.domain.track.interactor.RefreshTracks
|
||||
import eu.kanade.domain.track.interactor.SyncChapterProgressWithTrack
|
||||
import eu.kanade.domain.track.interactor.TrackChapter
|
||||
import tachiyomi.data.category.CategoryRepositoryImpl
|
||||
import tachiyomi.data.chapter.ChapterRepositoryImpl
|
||||
import tachiyomi.data.history.HistoryRepositoryImpl
|
||||
import tachiyomi.data.manga.MangaRepositoryImpl
|
||||
import tachiyomi.data.source.SourceDataRepositoryImpl
|
||||
import tachiyomi.data.release.ReleaseServiceImpl
|
||||
import tachiyomi.data.source.SourceRepositoryImpl
|
||||
import tachiyomi.data.source.StubSourceRepositoryImpl
|
||||
import tachiyomi.data.track.TrackRepositoryImpl
|
||||
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.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.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.UpdateChapter
|
||||
import tachiyomi.domain.chapter.repository.ChapterRepository
|
||||
import tachiyomi.domain.history.interactor.GetHistory
|
||||
import tachiyomi.domain.history.interactor.GetNextChapters
|
||||
import tachiyomi.domain.history.interactor.GetTotalReadDuration
|
||||
import tachiyomi.domain.history.interactor.RemoveHistory
|
||||
import tachiyomi.domain.history.interactor.UpsertHistory
|
||||
import tachiyomi.domain.history.repository.HistoryRepository
|
||||
import tachiyomi.domain.manga.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.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.updates.interactor.GetUpdates
|
||||
import tachiyomi.domain.updates.repository.UpdatesRepository
|
||||
@ -79,7 +95,7 @@ class DomainModule : InjektModule {
|
||||
addSingletonFactory<CategoryRepository> { CategoryRepositoryImpl(get()) }
|
||||
addFactory { GetCategories(get()) }
|
||||
addFactory { ResetCategoryFlags(get(), get()) }
|
||||
addFactory { SetDisplayModeForCategory(get(), get()) }
|
||||
addFactory { SetDisplayMode(get()) }
|
||||
addFactory { SetSortModeForCategory(get(), get()) }
|
||||
addFactory { CreateCategoryWithName(get(), get()) }
|
||||
addFactory { RenameCategory(get()) }
|
||||
@ -92,30 +108,42 @@ class DomainModule : InjektModule {
|
||||
addFactory { GetFavorites(get()) }
|
||||
addFactory { GetLibraryManga(get()) }
|
||||
addFactory { GetMangaWithChapters(get(), get()) }
|
||||
addFactory { GetMangaByUrlAndSourceId(get()) }
|
||||
addFactory { GetManga(get()) }
|
||||
addFactory { GetNextChapters(get(), get(), get()) }
|
||||
addFactory { ResetViewerFlags(get()) }
|
||||
addFactory { SetMangaChapterFlags(get()) }
|
||||
addFactory { FetchInterval(get()) }
|
||||
addFactory { SetMangaDefaultChapterFlags(get(), get(), get()) }
|
||||
addFactory { SetMangaViewerFlags(get()) }
|
||||
addFactory { NetworkToLocalManga(get()) }
|
||||
addFactory { UpdateManga(get()) }
|
||||
addFactory { UpdateManga(get(), get()) }
|
||||
addFactory { SetMangaCategories(get()) }
|
||||
addFactory { GetExcludedScanlators(get()) }
|
||||
addFactory { SetExcludedScanlators(get()) }
|
||||
|
||||
addSingletonFactory<ReleaseService> { ReleaseServiceImpl(get(), get()) }
|
||||
addFactory { GetApplicationRelease(get(), 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 { GetTracksPerManga(get()) }
|
||||
addFactory { GetTracks(get()) }
|
||||
addFactory { InsertTrack(get()) }
|
||||
addFactory { SyncChapterProgressWithTrack(get(), get(), get()) }
|
||||
|
||||
addSingletonFactory<ChapterRepository> { ChapterRepositoryImpl(get()) }
|
||||
addFactory { GetChapter(get()) }
|
||||
addFactory { GetChapterByMangaId(get()) }
|
||||
addFactory { GetChaptersByMangaId(get()) }
|
||||
addFactory { GetChapterByUrlAndMangaId(get()) }
|
||||
addFactory { UpdateChapter(get()) }
|
||||
addFactory { SetReadStatus(get(), get(), get(), get()) }
|
||||
addFactory { ShouldUpdateDbChapter() }
|
||||
addFactory { SyncChaptersWithSource(get(), get(), get(), get()) }
|
||||
addFactory { SyncChaptersWithTrackServiceTwoWay(get(), get()) }
|
||||
addFactory { SyncChaptersWithSource(get(), get(), get(), get(), get(), get(), get(), get()) }
|
||||
addFactory { GetAvailableScanlators(get()) }
|
||||
|
||||
addSingletonFactory<HistoryRepository> { HistoryRepositoryImpl(get()) }
|
||||
addFactory { GetHistory(get()) }
|
||||
@ -133,7 +161,7 @@ class DomainModule : InjektModule {
|
||||
addFactory { GetUpdates(get()) }
|
||||
|
||||
addSingletonFactory<SourceRepository> { SourceRepositoryImpl(get(), get()) }
|
||||
addSingletonFactory<SourceDataRepository> { SourceDataRepositoryImpl(get()) }
|
||||
addSingletonFactory<StubSourceRepository> { StubSourceRepositoryImpl(get()) }
|
||||
addFactory { GetEnabledSources(get(), get()) }
|
||||
addFactory { GetLanguagesWithSources(get(), get()) }
|
||||
addFactory { GetRemoteManga(get()) }
|
||||
@ -143,5 +171,10 @@ class DomainModule : InjektModule {
|
||||
addFactory { ToggleLanguage(get()) }
|
||||
addFactory { ToggleSource(get()) }
|
||||
addFactory { ToggleSourcePin(get()) }
|
||||
addFactory { TrustExtension(get()) }
|
||||
|
||||
addFactory { CreateExtensionRepo(get()) }
|
||||
addFactory { DeleteExtensionRepo(get()) }
|
||||
addFactory { GetExtensionRepos(get()) }
|
||||
}
|
||||
}
|
||||
|
@ -1,16 +0,0 @@
|
||||
package eu.kanade.domain.backup.service
|
||||
|
||||
import tachiyomi.core.preference.PreferenceStore
|
||||
import tachiyomi.core.provider.FolderProvider
|
||||
|
||||
class BackupPreferences(
|
||||
private val folderProvider: FolderProvider,
|
||||
private val preferenceStore: PreferenceStore,
|
||||
) {
|
||||
|
||||
fun backupsDirectory() = preferenceStore.getString("backup_directory", folderProvider.path())
|
||||
|
||||
fun numberOfBackups() = preferenceStore.getInt("backup_slots", 2)
|
||||
|
||||
fun backupInterval() = preferenceStore.getInt("backup_interval", 12)
|
||||
}
|
@ -1,24 +1,31 @@
|
||||
package eu.kanade.domain.base
|
||||
|
||||
import android.content.Context
|
||||
import eu.kanade.tachiyomi.util.system.isPreviewBuildType
|
||||
import eu.kanade.tachiyomi.util.system.isReleaseBuildType
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import tachiyomi.core.preference.Preference
|
||||
import tachiyomi.core.preference.PreferenceStore
|
||||
import tachiyomi.i18n.MR
|
||||
|
||||
class BasePreferences(
|
||||
val context: Context,
|
||||
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("incognito_mode", false)
|
||||
|
||||
fun automaticExtUpdates() = preferenceStore.getBoolean("automatic_ext_updates", true)
|
||||
fun incognitoMode() = preferenceStore.getBoolean(Preference.appStateKey("incognito_mode"), false)
|
||||
|
||||
fun extensionInstaller() = ExtensionInstallerPreference(context, preferenceStore)
|
||||
|
||||
fun acraEnabled() = preferenceStore.getBoolean("acra.enable", isPreviewBuildType || isReleaseBuildType)
|
||||
fun shownOnboardingFlow() = preferenceStore.getBoolean(Preference.appStateKey("onboarding_complete"), false)
|
||||
|
||||
enum class ExtensionInstaller(val titleRes: StringResource, val requiresSystemPermission: Boolean) {
|
||||
LEGACY(MR.strings.ext_installer_legacy, true),
|
||||
PACKAGEINSTALLER(MR.strings.ext_installer_packageinstaller, true),
|
||||
SHIZUKU(MR.strings.ext_installer_shizuku, false),
|
||||
PRIVATE(MR.strings.ext_installer_private, false),
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
package eu.kanade.domain.base
|
||||
|
||||
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.isShizukuInstalled
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@ -18,7 +18,7 @@ class ExtensionInstallerPreference(
|
||||
|
||||
override fun key() = "extension_installer"
|
||||
|
||||
val entries get() = ExtensionInstaller.values().run {
|
||||
val entries get() = ExtensionInstaller.entries.run {
|
||||
if (context.hasMiuiPackageInstaller) {
|
||||
filter { it != ExtensionInstaller.PACKAGEINSTALLER }
|
||||
} 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)
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
package eu.kanade.domain.chapter.interactor
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import tachiyomi.domain.chapter.repository.ChapterRepository
|
||||
|
||||
class GetAvailableScanlators(
|
||||
private val repository: ChapterRepository,
|
||||
) {
|
||||
|
||||
private fun List<String>.cleanupAvailableScanlators(): Set<String> {
|
||||
return mapNotNull { it.ifBlank { null } }.toSet()
|
||||
}
|
||||
|
||||
suspend fun await(mangaId: Long): Set<String> {
|
||||
return repository.getScanlatorsByMangaId(mangaId)
|
||||
.cleanupAvailableScanlators()
|
||||
}
|
||||
|
||||
fun subscribe(mangaId: Long): Flow<Set<String>> {
|
||||
return repository.getScanlatorsByMangaIdAsFlow(mangaId)
|
||||
.map { it.cleanupAvailableScanlators() }
|
||||
}
|
||||
}
|
@ -1,13 +1,13 @@
|
||||
package eu.kanade.domain.chapter.interactor
|
||||
|
||||
import eu.kanade.domain.download.interactor.DeleteDownload
|
||||
import eu.kanade.domain.download.service.DownloadPreferences
|
||||
import logcat.LogPriority
|
||||
import tachiyomi.core.util.lang.withNonCancellableContext
|
||||
import tachiyomi.core.util.system.logcat
|
||||
import tachiyomi.domain.chapter.model.Chapter
|
||||
import tachiyomi.domain.chapter.model.ChapterUpdate
|
||||
import tachiyomi.domain.chapter.repository.ChapterRepository
|
||||
import tachiyomi.domain.download.service.DownloadPreferences
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.domain.manga.repository.MangaRepository
|
||||
|
||||
@ -72,9 +72,9 @@ class SetReadStatus(
|
||||
suspend fun await(manga: Manga, read: Boolean) =
|
||||
await(manga.id, read)
|
||||
|
||||
sealed class Result {
|
||||
object Success : Result()
|
||||
object NoChapters : Result()
|
||||
data class InternalError(val error: Throwable) : Result()
|
||||
sealed interface Result {
|
||||
data object Success : Result
|
||||
data object NoChapters : Result
|
||||
data class InternalError(val error: Throwable) : Result
|
||||
}
|
||||
}
|
||||
|
@ -2,36 +2,38 @@ package eu.kanade.domain.chapter.interactor
|
||||
|
||||
import eu.kanade.domain.chapter.model.copyFromSChapter
|
||||
import eu.kanade.domain.chapter.model.toSChapter
|
||||
import eu.kanade.domain.manga.interactor.GetExcludedScanlators
|
||||
import eu.kanade.domain.manga.interactor.UpdateManga
|
||||
import eu.kanade.domain.manga.model.toSManga
|
||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||
import eu.kanade.tachiyomi.data.download.DownloadProvider
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.isLocal
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.util.chapter.ChapterRecognition
|
||||
import tachiyomi.data.chapter.ChapterSanitizer
|
||||
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
|
||||
import tachiyomi.domain.chapter.interactor.ShouldUpdateDbChapter
|
||||
import tachiyomi.domain.chapter.interactor.UpdateChapter
|
||||
import tachiyomi.domain.chapter.model.Chapter
|
||||
import tachiyomi.domain.chapter.model.NoChaptersException
|
||||
import tachiyomi.domain.chapter.model.toChapterUpdate
|
||||
import tachiyomi.domain.chapter.repository.ChapterRepository
|
||||
import tachiyomi.domain.chapter.service.ChapterRecognition
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import tachiyomi.source.local.isLocal
|
||||
import java.lang.Long.max
|
||||
import java.util.Date
|
||||
import java.time.ZonedDateTime
|
||||
import java.util.TreeSet
|
||||
|
||||
class SyncChaptersWithSource(
|
||||
private val downloadManager: DownloadManager = Injekt.get(),
|
||||
private val downloadProvider: DownloadProvider = Injekt.get(),
|
||||
private val chapterRepository: ChapterRepository = Injekt.get(),
|
||||
private val shouldUpdateDbChapter: ShouldUpdateDbChapter = Injekt.get(),
|
||||
private val updateManga: UpdateManga = Injekt.get(),
|
||||
private val updateChapter: UpdateChapter = Injekt.get(),
|
||||
private val getChapterByMangaId: GetChapterByMangaId = Injekt.get(),
|
||||
private val downloadManager: DownloadManager,
|
||||
private val downloadProvider: DownloadProvider,
|
||||
private val chapterRepository: ChapterRepository,
|
||||
private val shouldUpdateDbChapter: ShouldUpdateDbChapter,
|
||||
private val updateManga: UpdateManga,
|
||||
private val updateChapter: UpdateChapter,
|
||||
private val getChaptersByMangaId: GetChaptersByMangaId,
|
||||
private val getExcludedScanlators: GetExcludedScanlators,
|
||||
) {
|
||||
|
||||
/**
|
||||
@ -46,11 +48,16 @@ class SyncChaptersWithSource(
|
||||
rawSourceChapters: List<SChapter>,
|
||||
manga: Manga,
|
||||
source: Source,
|
||||
manualFetch: Boolean = false,
|
||||
fetchWindow: Pair<Long, Long> = Pair(0, 0),
|
||||
): List<Chapter> {
|
||||
if (rawSourceChapters.isEmpty() && !source.isLocal()) {
|
||||
throw NoChaptersException()
|
||||
}
|
||||
|
||||
val now = ZonedDateTime.now()
|
||||
val nowMillis = now.toInstant().toEpochMilli()
|
||||
|
||||
val sourceChapters = rawSourceChapters
|
||||
.distinctBy { it.url }
|
||||
.mapIndexed { i, sChapter ->
|
||||
@ -60,36 +67,27 @@ class SyncChaptersWithSource(
|
||||
.copy(mangaId = manga.id, sourceOrder = i.toLong())
|
||||
}
|
||||
|
||||
// Chapters from db.
|
||||
val dbChapters = getChapterByMangaId.await(manga.id)
|
||||
val dbChapters = getChaptersByMangaId.await(manga.id)
|
||||
|
||||
// Chapters from the source not in db.
|
||||
val toAdd = mutableListOf<Chapter>()
|
||||
|
||||
// Chapters whose metadata have changed.
|
||||
val toChange = mutableListOf<Chapter>()
|
||||
|
||||
// Chapters from the db not in source.
|
||||
val toDelete = dbChapters.filterNot { dbChapter ->
|
||||
val newChapters = mutableListOf<Chapter>()
|
||||
val updatedChapters = mutableListOf<Chapter>()
|
||||
val removedChapters = dbChapters.filterNot { dbChapter ->
|
||||
sourceChapters.any { sourceChapter ->
|
||||
dbChapter.url == sourceChapter.url
|
||||
}
|
||||
}
|
||||
|
||||
val rightNow = Date().time
|
||||
|
||||
// Used to not set upload date of older chapters
|
||||
// to a higher value than newer chapters
|
||||
var maxSeenUploadDate = 0L
|
||||
|
||||
val sManga = manga.toSManga()
|
||||
for (sourceChapter in sourceChapters) {
|
||||
var chapter = sourceChapter
|
||||
|
||||
// Update metadata from source if necessary.
|
||||
if (source is HttpSource) {
|
||||
val sChapter = chapter.toSChapter()
|
||||
source.prepareNewChapter(sChapter, sManga)
|
||||
source.prepareNewChapter(sChapter, manga.toSManga())
|
||||
chapter = chapter.copyFromSChapter(sChapter)
|
||||
}
|
||||
|
||||
@ -101,17 +99,19 @@ class SyncChaptersWithSource(
|
||||
|
||||
if (dbChapter == null) {
|
||||
val toAddChapter = if (chapter.dateUpload == 0L) {
|
||||
val altDateUpload = if (maxSeenUploadDate == 0L) rightNow else maxSeenUploadDate
|
||||
val altDateUpload = if (maxSeenUploadDate == 0L) nowMillis else maxSeenUploadDate
|
||||
chapter.copy(dateUpload = altDateUpload)
|
||||
} else {
|
||||
maxSeenUploadDate = max(maxSeenUploadDate, sourceChapter.dateUpload)
|
||||
chapter
|
||||
}
|
||||
toAdd.add(toAddChapter)
|
||||
newChapters.add(toAddChapter)
|
||||
} else {
|
||||
if (shouldUpdateDbChapter.await(dbChapter, chapter)) {
|
||||
val shouldRenameChapter = downloadProvider.isChapterDirNameChanged(dbChapter, chapter) &&
|
||||
downloadManager.isChapterDownloaded(dbChapter.name, dbChapter.scanlator, manga.title, manga.source)
|
||||
downloadManager.isChapterDownloaded(
|
||||
dbChapter.name, dbChapter.scanlator, manga.title, manga.source,
|
||||
)
|
||||
|
||||
if (shouldRenameChapter) {
|
||||
downloadManager.renameChapter(source, manga, dbChapter, chapter)
|
||||
@ -125,36 +125,43 @@ class SyncChaptersWithSource(
|
||||
if (chapter.dateUpload != 0L) {
|
||||
toChangeChapter = toChangeChapter.copy(dateUpload = chapter.dateUpload)
|
||||
}
|
||||
toChange.add(toChangeChapter)
|
||||
updatedChapters.add(toChangeChapter)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return if there's nothing to add, delete or change, avoiding unnecessary db transactions.
|
||||
if (toAdd.isEmpty() && toDelete.isEmpty() && toChange.isEmpty()) {
|
||||
// Return if there's nothing to add, delete, or update to avoid unnecessary db transactions.
|
||||
if (newChapters.isEmpty() && removedChapters.isEmpty() && updatedChapters.isEmpty()) {
|
||||
if (manualFetch || manga.fetchInterval == 0 || manga.nextUpdate < fetchWindow.first) {
|
||||
updateManga.awaitUpdateFetchInterval(
|
||||
manga,
|
||||
now,
|
||||
fetchWindow,
|
||||
)
|
||||
}
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
val reAdded = mutableListOf<Chapter>()
|
||||
|
||||
val deletedChapterNumbers = TreeSet<Float>()
|
||||
val deletedReadChapterNumbers = TreeSet<Float>()
|
||||
val deletedBookmarkedChapterNumbers = TreeSet<Float>()
|
||||
val deletedChapterNumbers = TreeSet<Double>()
|
||||
val deletedReadChapterNumbers = TreeSet<Double>()
|
||||
val deletedBookmarkedChapterNumbers = TreeSet<Double>()
|
||||
|
||||
toDelete.forEach { chapter ->
|
||||
removedChapters.forEach { chapter ->
|
||||
if (chapter.read) deletedReadChapterNumbers.add(chapter.chapterNumber)
|
||||
if (chapter.bookmark) deletedBookmarkedChapterNumbers.add(chapter.chapterNumber)
|
||||
deletedChapterNumbers.add(chapter.chapterNumber)
|
||||
}
|
||||
|
||||
val deletedChapterNumberDateFetchMap = toDelete.sortedByDescending { it.dateFetch }
|
||||
val deletedChapterNumberDateFetchMap = removedChapters.sortedByDescending { it.dateFetch }
|
||||
.associate { it.chapterNumber to it.dateFetch }
|
||||
|
||||
// Date fetch is set in such a way that the upper ones will have bigger value than the lower ones
|
||||
// Sources MUST return the chapters from most to less recent, which is common.
|
||||
var itemCount = toAdd.size
|
||||
var updatedToAdd = toAdd.map { toAddItem ->
|
||||
var chapter = toAddItem.copy(dateFetch = rightNow + itemCount--)
|
||||
var itemCount = newChapters.size
|
||||
var updatedToAdd = newChapters.map { toAddItem ->
|
||||
var chapter = toAddItem.copy(dateFetch = nowMillis + itemCount--)
|
||||
|
||||
if (chapter.isRecognizedNumber.not() || chapter.chapterNumber !in deletedChapterNumbers) return@map chapter
|
||||
|
||||
@ -173,8 +180,8 @@ class SyncChaptersWithSource(
|
||||
chapter
|
||||
}
|
||||
|
||||
if (toDelete.isNotEmpty()) {
|
||||
val toDeleteIds = toDelete.map { it.id }
|
||||
if (removedChapters.isNotEmpty()) {
|
||||
val toDeleteIds = removedChapters.map { it.id }
|
||||
chapterRepository.removeChaptersWithIds(toDeleteIds)
|
||||
}
|
||||
|
||||
@ -182,10 +189,11 @@ class SyncChaptersWithSource(
|
||||
updatedToAdd = chapterRepository.addAll(updatedToAdd)
|
||||
}
|
||||
|
||||
if (toChange.isNotEmpty()) {
|
||||
val chapterUpdates = toChange.map { it.toChapterUpdate() }
|
||||
if (updatedChapters.isNotEmpty()) {
|
||||
val chapterUpdates = updatedChapters.map { it.toChapterUpdate() }
|
||||
updateChapter.awaitAll(chapterUpdates)
|
||||
}
|
||||
updateManga.awaitUpdateFetchInterval(manga, now, fetchWindow)
|
||||
|
||||
// Set this manga as updated since chapters were changed
|
||||
// Note that last_update actually represents last time the chapter list changed at all
|
||||
@ -193,6 +201,10 @@ class SyncChaptersWithSource(
|
||||
|
||||
val reAddedUrls = reAdded.map { it.url }.toHashSet()
|
||||
|
||||
return updatedToAdd.filterNot { it.url in reAddedUrls }
|
||||
val excludedScanlators = getExcludedScanlators.await(manga.id).toHashSet()
|
||||
|
||||
return updatedToAdd.filterNot {
|
||||
it.url in reAddedUrls || it.scanlator in excludedScanlators
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ fun Chapter.toSChapter(): SChapter {
|
||||
it.url = url
|
||||
it.name = name
|
||||
it.date_upload = dateUpload
|
||||
it.chapter_number = chapterNumber
|
||||
it.chapter_number = chapterNumber.toFloat()
|
||||
it.scanlator = scanlator
|
||||
}
|
||||
}
|
||||
@ -21,8 +21,8 @@ fun Chapter.copyFromSChapter(sChapter: SChapter): Chapter {
|
||||
name = sChapter.name,
|
||||
url = sChapter.url,
|
||||
dateUpload = sChapter.date_upload,
|
||||
chapterNumber = sChapter.chapter_number,
|
||||
scanlator = sChapter.scanlator?.ifBlank { null },
|
||||
chapterNumber = sChapter.chapter_number.toDouble(),
|
||||
scanlator = sChapter.scanlator?.ifBlank { null }?.trim(),
|
||||
)
|
||||
}
|
||||
|
||||
@ -37,6 +37,6 @@ fun Chapter.toDbChapter(): DbChapter = ChapterImpl().also {
|
||||
it.last_page_read = lastPageRead.toInt()
|
||||
it.date_fetch = dateFetch
|
||||
it.date_upload = dateUpload
|
||||
it.chapter_number = chapterNumber
|
||||
it.chapter_number = chapterNumber.toFloat()
|
||||
it.source_order = sourceOrder.toInt()
|
||||
}
|
||||
|
@ -1,14 +1,13 @@
|
||||
package eu.kanade.domain.chapter.model
|
||||
|
||||
import eu.kanade.domain.manga.model.downloadedFilter
|
||||
import eu.kanade.domain.manga.model.isLocal
|
||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||
import eu.kanade.tachiyomi.data.download.model.Download
|
||||
import eu.kanade.tachiyomi.ui.manga.ChapterItem
|
||||
import eu.kanade.tachiyomi.util.chapter.getChapterSort
|
||||
import eu.kanade.tachiyomi.ui.manga.ChapterList
|
||||
import tachiyomi.domain.chapter.model.Chapter
|
||||
import tachiyomi.domain.chapter.service.getChapterSort
|
||||
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.
|
||||
@ -20,30 +19,17 @@ fun List<Chapter>.applyFilters(manga: Manga, downloadManager: DownloadManager):
|
||||
val downloadedFilter = manga.downloadedFilter
|
||||
val bookmarkedFilter = manga.bookmarkedFilter
|
||||
|
||||
return filter { chapter ->
|
||||
when (unreadFilter) {
|
||||
TriStateFilter.DISABLED -> true
|
||||
TriStateFilter.ENABLED_IS -> !chapter.read
|
||||
TriStateFilter.ENABLED_NOT -> chapter.read
|
||||
}
|
||||
}
|
||||
return filter { chapter -> applyFilter(unreadFilter) { !chapter.read } }
|
||||
.filter { chapter -> applyFilter(bookmarkedFilter) { chapter.bookmark } }
|
||||
.filter { chapter ->
|
||||
when (bookmarkedFilter) {
|
||||
TriStateFilter.DISABLED -> true
|
||||
TriStateFilter.ENABLED_IS -> chapter.bookmark
|
||||
TriStateFilter.ENABLED_NOT -> !chapter.bookmark
|
||||
}
|
||||
}
|
||||
.filter { chapter ->
|
||||
val downloaded = downloadManager.isChapterDownloaded(chapter.name, chapter.scanlator, manga.title, manga.source)
|
||||
val downloadState = when {
|
||||
downloaded -> Download.State.DOWNLOADED
|
||||
else -> Download.State.NOT_DOWNLOADED
|
||||
}
|
||||
when (downloadedFilter) {
|
||||
TriStateFilter.DISABLED -> true
|
||||
TriStateFilter.ENABLED_IS -> downloadState == Download.State.DOWNLOADED || isLocalManga
|
||||
TriStateFilter.ENABLED_NOT -> downloadState != Download.State.DOWNLOADED && !isLocalManga
|
||||
applyFilter(downloadedFilter) {
|
||||
val downloaded = downloadManager.isChapterDownloaded(
|
||||
chapter.name,
|
||||
chapter.scanlator,
|
||||
manga.title,
|
||||
manga.source,
|
||||
)
|
||||
downloaded || isLocalManga
|
||||
}
|
||||
}
|
||||
.sortedWith(getChapterSort(manga))
|
||||
@ -53,32 +39,14 @@ fun List<Chapter>.applyFilters(manga: Manga, downloadManager: DownloadManager):
|
||||
* Applies the view filters to the list of chapters obtained from the database.
|
||||
* @return an observable of the list of chapters filtered and sorted.
|
||||
*/
|
||||
fun List<ChapterItem>.applyFilters(manga: Manga): Sequence<ChapterItem> {
|
||||
fun List<ChapterList.Item>.applyFilters(manga: Manga): Sequence<ChapterList.Item> {
|
||||
val isLocalManga = manga.isLocal()
|
||||
val unreadFilter = manga.unreadFilter
|
||||
val downloadedFilter = manga.downloadedFilter
|
||||
val bookmarkedFilter = manga.bookmarkedFilter
|
||||
return asSequence()
|
||||
.filter { (chapter) ->
|
||||
when (unreadFilter) {
|
||||
TriStateFilter.DISABLED -> true
|
||||
TriStateFilter.ENABLED_IS -> !chapter.read
|
||||
TriStateFilter.ENABLED_NOT -> chapter.read
|
||||
}
|
||||
}
|
||||
.filter { (chapter) ->
|
||||
when (bookmarkedFilter) {
|
||||
TriStateFilter.DISABLED -> true
|
||||
TriStateFilter.ENABLED_IS -> chapter.bookmark
|
||||
TriStateFilter.ENABLED_NOT -> !chapter.bookmark
|
||||
}
|
||||
}
|
||||
.filter {
|
||||
when (downloadedFilter) {
|
||||
TriStateFilter.DISABLED -> true
|
||||
TriStateFilter.ENABLED_IS -> it.isDownloaded || isLocalManga
|
||||
TriStateFilter.ENABLED_NOT -> !it.isDownloaded && !isLocalManga
|
||||
}
|
||||
}
|
||||
.filter { (chapter) -> applyFilter(unreadFilter) { !chapter.read } }
|
||||
.filter { (chapter) -> applyFilter(bookmarkedFilter) { chapter.bookmark } }
|
||||
.filter { applyFilter(downloadedFilter) { it.isDownloaded || isLocalManga } }
|
||||
.sortedWith { (chapter1), (chapter2) -> getChapterSort(manga).invoke(chapter1, chapter2) }
|
||||
}
|
||||
|
@ -1,10 +1,10 @@
|
||||
package eu.kanade.domain.download.interactor
|
||||
|
||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import tachiyomi.core.util.lang.withNonCancellableContext
|
||||
import tachiyomi.domain.chapter.model.Chapter
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.domain.source.service.SourceManager
|
||||
|
||||
class DeleteDownload(
|
||||
private val sourceManager: SourceManager,
|
||||
|
@ -0,0 +1,25 @@
|
||||
package eu.kanade.domain.extension.interactor
|
||||
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import tachiyomi.core.preference.plusAssign
|
||||
|
||||
class CreateExtensionRepo(private val preferences: SourcePreferences) {
|
||||
|
||||
fun await(name: String): Result {
|
||||
// Do not allow invalid formats
|
||||
if (!name.matches(repoRegex)) {
|
||||
return Result.InvalidUrl
|
||||
}
|
||||
|
||||
preferences.extensionRepos() += name.removeSuffix("/index.min.json")
|
||||
|
||||
return Result.Success
|
||||
}
|
||||
|
||||
sealed interface Result {
|
||||
data object InvalidUrl : Result
|
||||
data object Success : Result
|
||||
}
|
||||
}
|
||||
|
||||
private val repoRegex = """^https://.*/index\.min\.json$""".toRegex()
|
@ -0,0 +1,11 @@
|
||||
package eu.kanade.domain.extension.interactor
|
||||
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import tachiyomi.core.preference.minusAssign
|
||||
|
||||
class DeleteExtensionRepo(private val preferences: SourcePreferences) {
|
||||
|
||||
fun await(repo: String) {
|
||||
preferences.extensionRepos() -= repo
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
package eu.kanade.domain.extension.interactor
|
||||
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
class GetExtensionRepos(private val preferences: SourcePreferences) {
|
||||
|
||||
fun subscribe(): Flow<Set<String>> {
|
||||
return preferences.extensionRepos().changes()
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
package eu.kanade.domain.extension.interactor
|
||||
|
||||
import android.content.pm.PackageInfo
|
||||
import androidx.core.content.pm.PackageInfoCompat
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import tachiyomi.core.preference.getAndSet
|
||||
|
||||
class TrustExtension(
|
||||
private val preferences: SourcePreferences,
|
||||
) {
|
||||
|
||||
fun isTrusted(pkgInfo: PackageInfo, signatureHash: String): Boolean {
|
||||
val key = "${pkgInfo.packageName}:${PackageInfoCompat.getLongVersionCode(pkgInfo)}:$signatureHash"
|
||||
return key in preferences.trustedExtensions().get()
|
||||
}
|
||||
|
||||
fun trust(pkgName: String, versionCode: Long, signatureHash: String) {
|
||||
preferences.trustedExtensions().getAndSet { exts ->
|
||||
// Remove previously trusted versions
|
||||
val removed = exts.filterNot { it.startsWith("$pkgName:") }.toMutableSet()
|
||||
|
||||
removed.also {
|
||||
it += "$pkgName:$versionCode:$signatureHash"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun revokeAll() {
|
||||
preferences.trustedExtensions().delete()
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
package eu.kanade.domain.manga.interactor
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import tachiyomi.data.DatabaseHandler
|
||||
|
||||
class GetExcludedScanlators(
|
||||
private val handler: DatabaseHandler,
|
||||
) {
|
||||
|
||||
suspend fun await(mangaId: Long): Set<String> {
|
||||
return handler.awaitList {
|
||||
excluded_scanlatorsQueries.getExcludedScanlatorsByMangaId(mangaId)
|
||||
}
|
||||
.toSet()
|
||||
}
|
||||
|
||||
fun subscribe(mangaId: Long): Flow<Set<String>> {
|
||||
return handler.subscribeToList {
|
||||
excluded_scanlatorsQueries.getExcludedScanlatorsByMangaId(mangaId)
|
||||
}
|
||||
.map { it.toSet() }
|
||||
}
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
package eu.kanade.domain.manga.interactor
|
||||
|
||||
import tachiyomi.data.DatabaseHandler
|
||||
|
||||
class SetExcludedScanlators(
|
||||
private val handler: DatabaseHandler,
|
||||
) {
|
||||
|
||||
suspend fun await(mangaId: Long, excludedScanlators: Set<String>) {
|
||||
handler.await(inTransaction = true) {
|
||||
val currentExcluded = handler.awaitList {
|
||||
excluded_scanlatorsQueries.getExcludedScanlatorsByMangaId(mangaId)
|
||||
}.toSet()
|
||||
val toAdd = excludedScanlators.minus(currentExcluded)
|
||||
for (scanlator in toAdd) {
|
||||
excluded_scanlatorsQueries.insert(mangaId, scanlator)
|
||||
}
|
||||
val toRemove = currentExcluded.minus(excludedScanlators)
|
||||
excluded_scanlatorsQueries.remove(mangaId, toRemove)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
package eu.kanade.domain.manga.interactor
|
||||
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode
|
||||
import tachiyomi.domain.manga.model.MangaUpdate
|
||||
import tachiyomi.domain.manga.repository.MangaRepository
|
||||
|
||||
@ -9,22 +9,22 @@ class SetMangaViewerFlags(
|
||||
private val mangaRepository: MangaRepository,
|
||||
) {
|
||||
|
||||
suspend fun awaitSetMangaReadingMode(id: Long, flag: Long) {
|
||||
suspend fun awaitSetReadingMode(id: Long, flag: Long) {
|
||||
val manga = mangaRepository.getMangaById(id)
|
||||
mangaRepository.update(
|
||||
MangaUpdate(
|
||||
id = id,
|
||||
viewerFlags = manga.viewerFlags.setFlag(flag, ReadingModeType.MASK.toLong()),
|
||||
viewerFlags = manga.viewerFlags.setFlag(flag, ReadingMode.MASK.toLong()),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun awaitSetOrientationType(id: Long, flag: Long) {
|
||||
suspend fun awaitSetOrientation(id: Long, flag: Long) {
|
||||
val manga = mangaRepository.getMangaById(id)
|
||||
mangaRepository.update(
|
||||
MangaUpdate(
|
||||
id = id,
|
||||
viewerFlags = manga.viewerFlags.setFlag(flag, OrientationType.MASK.toLong()),
|
||||
viewerFlags = manga.viewerFlags.setFlag(flag, ReaderOrientation.MASK.toLong()),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
@ -1,18 +1,21 @@
|
||||
package eu.kanade.domain.manga.interactor
|
||||
|
||||
import eu.kanade.domain.manga.model.hasCustomCover
|
||||
import eu.kanade.domain.manga.model.isLocal
|
||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import tachiyomi.domain.manga.interactor.FetchInterval
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.domain.manga.model.MangaUpdate
|
||||
import tachiyomi.domain.manga.repository.MangaRepository
|
||||
import tachiyomi.source.local.isLocal
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.util.Date
|
||||
import java.time.Instant
|
||||
import java.time.ZonedDateTime
|
||||
|
||||
class UpdateManga(
|
||||
private val mangaRepository: MangaRepository,
|
||||
private val fetchInterval: FetchInterval,
|
||||
) {
|
||||
|
||||
suspend fun await(mangaUpdate: MangaUpdate): Boolean {
|
||||
@ -43,14 +46,14 @@ class UpdateManga(
|
||||
// Never refresh covers if the url is empty to avoid "losing" existing covers
|
||||
remoteManga.thumbnail_url.isNullOrEmpty() -> null
|
||||
!manualFetch && localManga.thumbnailUrl == remoteManga.thumbnail_url -> null
|
||||
localManga.isLocal() -> Date().time
|
||||
localManga.isLocal() -> Instant.now().toEpochMilli()
|
||||
localManga.hasCustomCover(coverCache) -> {
|
||||
coverCache.deleteFromCache(localManga, false)
|
||||
null
|
||||
}
|
||||
else -> {
|
||||
coverCache.deleteFromCache(localManga, false)
|
||||
Date().time
|
||||
Instant.now().toEpochMilli()
|
||||
}
|
||||
}
|
||||
|
||||
@ -73,17 +76,27 @@ class UpdateManga(
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun awaitUpdateFetchInterval(
|
||||
manga: Manga,
|
||||
dateTime: ZonedDateTime = ZonedDateTime.now(),
|
||||
window: Pair<Long, Long> = fetchInterval.getWindow(dateTime),
|
||||
): Boolean {
|
||||
return mangaRepository.update(
|
||||
fetchInterval.toMangaUpdate(manga, dateTime, window),
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun awaitUpdateLastUpdate(mangaId: Long): Boolean {
|
||||
return mangaRepository.update(MangaUpdate(id = mangaId, lastUpdate = Date().time))
|
||||
return mangaRepository.update(MangaUpdate(id = mangaId, lastUpdate = Instant.now().toEpochMilli()))
|
||||
}
|
||||
|
||||
suspend fun awaitUpdateCoverLastModified(mangaId: Long): Boolean {
|
||||
return mangaRepository.update(MangaUpdate(id = mangaId, coverLastModified = Date().time))
|
||||
return mangaRepository.update(MangaUpdate(id = mangaId, coverLastModified = Instant.now().toEpochMilli()))
|
||||
}
|
||||
|
||||
suspend fun awaitUpdateFavorite(mangaId: Long, favorite: Boolean): Boolean {
|
||||
val dateAdded = when (favorite) {
|
||||
true -> Date().time
|
||||
true -> Instant.now().toEpochMilli()
|
||||
false -> 0
|
||||
}
|
||||
return mangaRepository.update(
|
||||
|
@ -2,35 +2,37 @@ package eu.kanade.domain.manga.model
|
||||
|
||||
import eu.kanade.domain.base.BasePreferences
|
||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||
import eu.kanade.tachiyomi.source.LocalSource
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode
|
||||
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.TriStateFilter
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
// TODO: move these into the domain model
|
||||
val Manga.readingModeType: Long
|
||||
get() = viewerFlags and ReadingModeType.MASK.toLong()
|
||||
val Manga.readingMode: Long
|
||||
get() = viewerFlags and ReadingMode.MASK.toLong()
|
||||
|
||||
val Manga.orientationType: Long
|
||||
get() = viewerFlags and OrientationType.MASK.toLong()
|
||||
val Manga.readerOrientation: Long
|
||||
get() = viewerFlags and ReaderOrientation.MASK.toLong()
|
||||
|
||||
val Manga.downloadedFilter: TriStateFilter
|
||||
val Manga.downloadedFilter: TriState
|
||||
get() {
|
||||
if (forceDownloaded()) return TriStateFilter.ENABLED_IS
|
||||
if (forceDownloaded()) return TriState.ENABLED_IS
|
||||
return when (downloadedFilterRaw) {
|
||||
Manga.CHAPTER_SHOW_DOWNLOADED -> TriStateFilter.ENABLED_IS
|
||||
Manga.CHAPTER_SHOW_NOT_DOWNLOADED -> TriStateFilter.ENABLED_NOT
|
||||
else -> TriStateFilter.DISABLED
|
||||
Manga.CHAPTER_SHOW_DOWNLOADED -> TriState.ENABLED_IS
|
||||
Manga.CHAPTER_SHOW_NOT_DOWNLOADED -> TriState.ENABLED_NOT
|
||||
else -> TriState.DISABLED
|
||||
}
|
||||
}
|
||||
fun Manga.chaptersFiltered(): Boolean {
|
||||
return unreadFilter != TriStateFilter.DISABLED ||
|
||||
downloadedFilter != TriStateFilter.DISABLED ||
|
||||
bookmarkedFilter != TriStateFilter.DISABLED
|
||||
return unreadFilter != TriState.DISABLED ||
|
||||
downloadedFilter != TriState.DISABLED ||
|
||||
bookmarkedFilter != TriState.DISABLED
|
||||
}
|
||||
fun Manga.forceDownloaded(): Boolean {
|
||||
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 {
|
||||
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
|
||||
|
||||
import eu.kanade.domain.source.repository.SourceRepository
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import eu.kanade.tachiyomi.source.LocalSource
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import tachiyomi.domain.source.model.Pin
|
||||
import tachiyomi.domain.source.model.Pins
|
||||
import tachiyomi.domain.source.model.Source
|
||||
import tachiyomi.domain.source.repository.SourceRepository
|
||||
import tachiyomi.source.local.isLocal
|
||||
|
||||
class GetEnabledSources(
|
||||
private val repository: SourceRepository,
|
||||
@ -24,7 +24,7 @@ class GetEnabledSources(
|
||||
repository.getSources(),
|
||||
) { pinnedSourceIds, enabledLanguages, disabledSources, lastUsedSource, sources ->
|
||||
sources
|
||||
.filter { it.lang in enabledLanguages || it.id == LocalSource.ID }
|
||||
.filter { it.lang in enabledLanguages || it.isLocal() }
|
||||
.filterNot { it.id.toString() in disabledSources }
|
||||
.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name })
|
||||
.flatMap {
|
||||
|
@ -1,18 +1,19 @@
|
||||
package eu.kanade.domain.source.interactor
|
||||
|
||||
import eu.kanade.domain.source.repository.SourceRepository
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import tachiyomi.domain.source.model.Source
|
||||
import tachiyomi.domain.source.repository.SourceRepository
|
||||
import java.util.SortedMap
|
||||
|
||||
class GetLanguagesWithSources(
|
||||
private val repository: SourceRepository,
|
||||
private val preferences: SourcePreferences,
|
||||
) {
|
||||
|
||||
fun subscribe(): Flow<Map<String, List<Source>>> {
|
||||
fun subscribe(): Flow<SortedMap<String, List<Source>>> {
|
||||
return combine(
|
||||
preferences.enabledLanguages().changes(),
|
||||
preferences.disabledSources().changes(),
|
||||
@ -23,7 +24,8 @@ class GetLanguagesWithSources(
|
||||
.thenBy(String.CASE_INSENSITIVE_ORDER) { it.name },
|
||||
)
|
||||
|
||||
sortedSources.groupBy { it.lang }
|
||||
sortedSources
|
||||
.groupBy { it.lang }
|
||||
.toSortedMap(
|
||||
compareBy<String> { it !in enabledLanguage }.then(LocaleHelper.comparator),
|
||||
)
|
||||
|
@ -1,13 +1,13 @@
|
||||
package eu.kanade.domain.source.interactor
|
||||
|
||||
import eu.kanade.domain.source.repository.SourceRepository
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import tachiyomi.core.util.lang.compareToWithCollator
|
||||
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.Locale
|
||||
|
||||
class GetSourcesWithFavoriteCount(
|
||||
private val repository: SourceRepository,
|
||||
@ -20,7 +20,9 @@ class GetSourcesWithFavoriteCount(
|
||||
preferences.migrationSortingMode().changes(),
|
||||
repository.getSourcesWithFavoriteCount(),
|
||||
) { 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,
|
||||
sorting: SetMigrateSorting.Mode,
|
||||
): java.util.Comparator<Pair<Source, Long>> {
|
||||
val locale = Locale.getDefault()
|
||||
val collator = Collator.getInstance(locale).apply {
|
||||
strength = Collator.PRIMARY
|
||||
}
|
||||
val sortFn: (Pair<Source, Long>, Pair<Source, Long>) -> Int = { a, b ->
|
||||
when (sorting) {
|
||||
SetMigrateSorting.Mode.ALPHABETICAL -> {
|
||||
when {
|
||||
a.first.isStub && b.first.isStub.not() -> -1
|
||||
b.first.isStub && a.first.isStub.not() -> 1
|
||||
else -> collator.compare(a.first.name.lowercase(locale), b.first.name.lowercase(locale))
|
||||
else -> a.first.name.lowercase().compareToWithCollator(b.first.name.lowercase())
|
||||
}
|
||||
}
|
||||
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.tachiyomi.util.system.LocaleHelper
|
||||
import tachiyomi.core.preference.Preference
|
||||
import tachiyomi.core.preference.PreferenceStore
|
||||
import tachiyomi.core.preference.getEnum
|
||||
import tachiyomi.domain.library.model.LibraryDisplayMode
|
||||
@ -10,7 +11,12 @@ class SourcePreferences(
|
||||
private val preferenceStore: PreferenceStore,
|
||||
) {
|
||||
|
||||
fun sourceDisplayMode() = preferenceStore.getObject("pref_display_mode_catalogue", LibraryDisplayMode.default, LibraryDisplayMode.Serializer::serialize, LibraryDisplayMode.Serializer::deserialize)
|
||||
fun sourceDisplayMode() = preferenceStore.getObject(
|
||||
"pref_display_mode_catalogue",
|
||||
LibraryDisplayMode.default,
|
||||
LibraryDisplayMode.Serializer::serialize,
|
||||
LibraryDisplayMode.Serializer::deserialize,
|
||||
)
|
||||
|
||||
fun enabledLanguages() = preferenceStore.getStringSet("source_languages", LocaleHelper.getDefaultEnabledLanguages())
|
||||
|
||||
@ -18,17 +24,28 @@ class SourcePreferences(
|
||||
|
||||
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 migrationSortingMode() = preferenceStore.getEnum("pref_migration_sorting", SetMigrateSorting.Mode.ALPHABETICAL)
|
||||
|
||||
fun migrationSortingDirection() = preferenceStore.getEnum("pref_migration_direction", SetMigrateSorting.Direction.ASCENDING)
|
||||
fun migrationSortingDirection() = preferenceStore.getEnum(
|
||||
"pref_migration_direction",
|
||||
SetMigrateSorting.Direction.ASCENDING,
|
||||
)
|
||||
|
||||
fun hideInLibraryItems() = preferenceStore.getBoolean("browse_hide_in_library_items", false)
|
||||
|
||||
fun extensionRepos() = preferenceStore.getStringSet("extension_repos", emptySet())
|
||||
|
||||
fun extensionUpdatesCount() = preferenceStore.getInt("ext_updates_count", 0)
|
||||
|
||||
fun trustedSignatures() = preferenceStore.getStringSet("trusted_signatures", emptySet())
|
||||
|
||||
fun searchPinnedSourcesOnly() = preferenceStore.getBoolean("search_pinned_sources_only", false)
|
||||
fun trustedExtensions() = preferenceStore.getStringSet(
|
||||
Preference.appStateKey("trusted_extensions"),
|
||||
emptySet(),
|
||||
)
|
||||
}
|
||||
|
107
app/src/main/java/eu/kanade/domain/track/interactor/AddTracks.kt
Normal file
107
app/src/main/java/eu/kanade/domain/track/interactor/AddTracks.kt
Normal file
@ -0,0 +1,107 @@
|
||||
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.trackerId) }
|
||||
.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.tachiyomi.data.track.TrackService
|
||||
import eu.kanade.tachiyomi.data.track.EnhancedTracker
|
||||
import eu.kanade.tachiyomi.data.track.Tracker
|
||||
import logcat.LogPriority
|
||||
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.track.interactor.InsertTrack
|
||||
import tachiyomi.domain.track.model.Track
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class SyncChaptersWithTrackServiceTwoWay(
|
||||
private val updateChapter: UpdateChapter = Injekt.get(),
|
||||
private val insertTrack: InsertTrack = Injekt.get(),
|
||||
class SyncChapterProgressWithTrack(
|
||||
private val updateChapter: UpdateChapter,
|
||||
private val insertTrack: InsertTrack,
|
||||
private val getChaptersByMangaId: GetChaptersByMangaId,
|
||||
) {
|
||||
|
||||
suspend fun await(
|
||||
chapters: List<Chapter>,
|
||||
mangaId: Long,
|
||||
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
|
||||
.filter { chapter -> chapter.chapterNumber <= remoteTrack.lastChapterRead && !chapter.read }
|
||||
.map { it.copy(read = true).toChapterUpdate() }
|
||||
@ -31,7 +39,7 @@ class SyncChaptersWithTrackServiceTwoWay(
|
||||
val updatedTrack = remoteTrack.copy(lastChapterRead = localLastRead.toDouble())
|
||||
|
||||
try {
|
||||
service.update(updatedTrack.toDbTrack())
|
||||
tracker.update(updatedTrack.toDbTrack())
|
||||
updateChapter.awaitAll(chapterUpdates)
|
||||
insertTrack.await(updatedTrack)
|
||||
} 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.trackerId)
|
||||
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) }
|
||||
}
|
||||
}
|
||||
}
|
@ -13,16 +13,16 @@ fun Track.copyPersonalFrom(other: Track): Track {
|
||||
)
|
||||
}
|
||||
|
||||
fun Track.toDbTrack(): DbTrack = DbTrack.create(syncId).also {
|
||||
fun Track.toDbTrack(): DbTrack = DbTrack.create(trackerId).also {
|
||||
it.id = id
|
||||
it.manga_id = mangaId
|
||||
it.media_id = remoteId
|
||||
it.remote_id = remoteId
|
||||
it.library_id = libraryId
|
||||
it.title = title
|
||||
it.last_chapter_read = lastChapterRead.toFloat()
|
||||
it.total_chapters = totalChapters.toInt()
|
||||
it.status = status.toInt()
|
||||
it.score = score
|
||||
it.score = score.toFloat()
|
||||
it.tracking_url = remoteUrl
|
||||
it.started_reading_date = startDate
|
||||
it.finished_reading_date = finishDate
|
||||
@ -33,14 +33,16 @@ fun DbTrack.toDomainTrack(idRequired: Boolean = true): Track? {
|
||||
return Track(
|
||||
id = trackId,
|
||||
mangaId = manga_id,
|
||||
syncId = sync_id.toLong(),
|
||||
remoteId = media_id,
|
||||
trackerId = tracker_id.toLong(),
|
||||
remoteId = remote_id,
|
||||
libraryId = library_id,
|
||||
title = title,
|
||||
lastChapterRead = last_chapter_read.toDouble(),
|
||||
totalChapters = total_chapters.toLong(),
|
||||
status = status.toLong(),
|
||||
score = score,
|
||||
// Jank workaround due to precision issues while converting
|
||||
// See https://github.com/tachiyomiorg/tachiyomi/issues/10343
|
||||
score = score.toString().toDouble(),
|
||||
remoteUrl = tracking_url,
|
||||
startDate = started_reading_date,
|
||||
finishDate = finished_reading_date,
|
||||
|
@ -7,72 +7,66 @@ import androidx.work.CoroutineWorker
|
||||
import androidx.work.ExistingWorkPolicy
|
||||
import androidx.work.NetworkType
|
||||
import androidx.work.OneTimeWorkRequestBuilder
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.WorkerParameters
|
||||
import eu.kanade.domain.track.interactor.GetTracks
|
||||
import eu.kanade.domain.track.interactor.InsertTrack
|
||||
import eu.kanade.domain.track.model.toDbTrack
|
||||
import eu.kanade.domain.track.interactor.TrackChapter
|
||||
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 tachiyomi.core.util.lang.withIOContext
|
||||
import tachiyomi.core.util.system.logcat
|
||||
import tachiyomi.domain.track.interactor.GetTracks
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class DelayedTrackingUpdateJob(context: Context, workerParams: WorkerParameters) :
|
||||
class DelayedTrackingUpdateJob(private val context: Context, workerParams: WorkerParameters) :
|
||||
CoroutineWorker(context, workerParams) {
|
||||
|
||||
override suspend fun doWork(): Result {
|
||||
val getTracks = Injekt.get<GetTracks>()
|
||||
val insertTrack = Injekt.get<InsertTrack>()
|
||||
if (runAttemptCount > 3) {
|
||||
return Result.failure()
|
||||
}
|
||||
|
||||
val getTracks = Injekt.get<GetTracks>()
|
||||
val trackChapter = Injekt.get<TrackChapter>()
|
||||
|
||||
val trackManager = Injekt.get<TrackManager>()
|
||||
val delayedTrackingStore = Injekt.get<DelayedTrackingStore>()
|
||||
|
||||
withIOContext {
|
||||
val tracks = delayedTrackingStore.getItems().mapNotNull {
|
||||
delayedTrackingStore.getItems()
|
||||
.mapNotNull {
|
||||
val track = getTracks.awaitOne(it.trackId)
|
||||
if (track == null) {
|
||||
delayedTrackingStore.remove(it.trackId)
|
||||
}
|
||||
track
|
||||
track?.copy(lastChapterRead = it.lastChapterRead.toDouble())
|
||||
}
|
||||
|
||||
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)
|
||||
} 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 {
|
||||
private const val TAG = "DelayedTrackingUpdate"
|
||||
|
||||
fun setupTask(context: Context) {
|
||||
val constraints = Constraints.Builder()
|
||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||
.build()
|
||||
val constraints = Constraints(
|
||||
requiredNetworkType = NetworkType.CONNECTED,
|
||||
)
|
||||
|
||||
val request = OneTimeWorkRequestBuilder<DelayedTrackingUpdateJob>()
|
||||
.setConstraints(constraints)
|
||||
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 20, TimeUnit.SECONDS)
|
||||
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 5, TimeUnit.MINUTES)
|
||||
.addTag(TAG)
|
||||
.build()
|
||||
|
||||
WorkManager.getInstance(context)
|
||||
.enqueueUniqueWork(TAG, ExistingWorkPolicy.REPLACE, request)
|
||||
context.workManager.enqueueUniqueWork(TAG, ExistingWorkPolicy.REPLACE, request)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,33 +1,32 @@
|
||||
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 tachiyomi.core.preference.Preference
|
||||
import tachiyomi.core.preference.PreferenceStore
|
||||
|
||||
class TrackPreferences(
|
||||
private val preferenceStore: PreferenceStore,
|
||||
) {
|
||||
|
||||
fun trackUsername(sync: TrackService) = preferenceStore.getString(trackUsername(sync.id), "")
|
||||
fun trackUsername(tracker: Tracker) = preferenceStore.getString(
|
||||
Preference.privateKey("pref_mangasync_username_${tracker.id}"),
|
||||
"",
|
||||
)
|
||||
|
||||
fun trackPassword(sync: TrackService) = preferenceStore.getString(trackPassword(sync.id), "")
|
||||
fun trackPassword(tracker: Tracker) = preferenceStore.getString(
|
||||
Preference.privateKey("pref_mangasync_password_${tracker.id}"),
|
||||
"",
|
||||
)
|
||||
|
||||
fun setTrackCredentials(sync: TrackService, username: String, password: String) {
|
||||
trackUsername(sync).set(username)
|
||||
trackPassword(sync).set(password)
|
||||
fun setCredentials(tracker: Tracker, username: String, password: String) {
|
||||
trackUsername(tracker).set(username)
|
||||
trackPassword(tracker).set(password)
|
||||
}
|
||||
|
||||
fun trackToken(sync: TrackService) = preferenceStore.getString(trackToken(sync.id), "")
|
||||
fun trackToken(tracker: Tracker) = preferenceStore.getString(Preference.privateKey("track_token_${tracker.id}"), "")
|
||||
|
||||
fun anilistScoreType() = preferenceStore.getString("anilist_score_type", Anilist.POINT_10)
|
||||
|
||||
fun autoUpdateTrack() = preferenceStore.getBoolean("pref_auto_update_manga_sync_key", true)
|
||||
|
||||
companion object {
|
||||
fun trackUsername(syncId: Long) = "pref_mangasync_username_$syncId"
|
||||
|
||||
private fun trackPassword(syncId: Long) = "pref_mangasync_password_$syncId"
|
||||
|
||||
private fun trackToken(syncId: Long) = "track_token_$syncId"
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,6 @@ import android.content.Context
|
||||
import androidx.core.content.edit
|
||||
import logcat.LogPriority
|
||||
import tachiyomi.core.util.system.logcat
|
||||
import tachiyomi.domain.track.model.Track
|
||||
|
||||
class DelayedTrackingStore(context: Context) {
|
||||
|
||||
@ -13,13 +12,12 @@ class DelayedTrackingStore(context: Context) {
|
||||
*/
|
||||
private val preferences = context.getSharedPreferences("tracking_queue", Context.MODE_PRIVATE)
|
||||
|
||||
fun addItem(track: Track) {
|
||||
val trackId = track.id.toString()
|
||||
val lastChapterRead = preferences.getFloat(trackId, 0f)
|
||||
if (track.lastChapterRead > lastChapterRead) {
|
||||
logcat(LogPriority.DEBUG) { "Queuing track item: $trackId, last chapter read: ${track.lastChapterRead}" }
|
||||
fun add(trackId: Long, lastChapterRead: Double) {
|
||||
val previousLastChapterRead = preferences.getFloat(trackId.toString(), 0f)
|
||||
if (lastChapterRead > previousLastChapterRead) {
|
||||
logcat(LogPriority.DEBUG) { "Queuing track item: $trackId, last chapter read: $lastChapterRead" }
|
||||
preferences.edit {
|
||||
putFloat(trackId, track.lastChapterRead.toFloat())
|
||||
putFloat(trackId.toString(), lastChapterRead.toFloat())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -28,7 +28,7 @@ class UiPreferences(
|
||||
|
||||
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", "")
|
||||
|
||||
|
@ -1,19 +1,25 @@
|
||||
package eu.kanade.domain.ui.model
|
||||
|
||||
import eu.kanade.tachiyomi.R
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import eu.kanade.tachiyomi.util.system.isDevFlavor
|
||||
import eu.kanade.tachiyomi.util.system.isPreviewBuildType
|
||||
import tachiyomi.i18n.MR
|
||||
|
||||
enum class AppTheme(val titleResId: Int?) {
|
||||
DEFAULT(R.string.label_default),
|
||||
MONET(R.string.theme_monet),
|
||||
GREEN_APPLE(R.string.theme_greenapple),
|
||||
LAVENDER(R.string.theme_lavender),
|
||||
MIDNIGHT_DUSK(R.string.theme_midnightdusk),
|
||||
STRAWBERRY_DAIQUIRI(R.string.theme_strawberrydaiquiri),
|
||||
TAKO(R.string.theme_tako),
|
||||
TEALTURQUOISE(R.string.theme_tealturquoise),
|
||||
TIDAL_WAVE(R.string.theme_tidalwave),
|
||||
YINYANG(R.string.theme_yinyang),
|
||||
YOTSUBA(R.string.theme_yotsuba),
|
||||
enum class AppTheme(val titleRes: StringResource?) {
|
||||
DEFAULT(MR.strings.label_default),
|
||||
MONET(MR.strings.theme_monet),
|
||||
GREEN_APPLE(MR.strings.theme_greenapple),
|
||||
LAVENDER(MR.strings.theme_lavender),
|
||||
MIDNIGHT_DUSK(MR.strings.theme_midnightdusk),
|
||||
|
||||
// TODO: re-enable for preview
|
||||
NORD(MR.strings.theme_nord.takeIf { isDevFlavor || isPreviewBuildType }),
|
||||
STRAWBERRY_DAIQUIRI(MR.strings.theme_strawberrydaiquiri),
|
||||
TAKO(MR.strings.theme_tako),
|
||||
TEALTURQUOISE(MR.strings.theme_tealturquoise),
|
||||
TIDAL_WAVE(MR.strings.theme_tidalwave),
|
||||
YINYANG(MR.strings.theme_yinyang),
|
||||
YOTSUBA(MR.strings.theme_yotsuba),
|
||||
|
||||
// Deprecated
|
||||
DARK_BLUE(null),
|
||||
|
@ -1,10 +1,11 @@
|
||||
package eu.kanade.domain.ui.model
|
||||
|
||||
import eu.kanade.tachiyomi.R
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import tachiyomi.i18n.MR
|
||||
|
||||
enum class TabletUiMode(val titleResId: Int) {
|
||||
AUTOMATIC(R.string.automatic_background),
|
||||
ALWAYS(R.string.lock_always),
|
||||
LANDSCAPE(R.string.landscape),
|
||||
NEVER(R.string.lock_never),
|
||||
enum class TabletUiMode(val titleRes: StringResource) {
|
||||
AUTOMATIC(MR.strings.automatic_background),
|
||||
ALWAYS(MR.strings.lock_always),
|
||||
LANDSCAPE(MR.strings.landscape),
|
||||
NEVER(MR.strings.lock_never),
|
||||
}
|
||||
|
@ -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,9 +1,10 @@
|
||||
package eu.kanade.presentation.browse
|
||||
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.HelpOutline
|
||||
import androidx.compose.material.icons.automirrored.outlined.HelpOutline
|
||||
import androidx.compose.material.icons.outlined.Public
|
||||
import androidx.compose.material.icons.outlined.Refresh
|
||||
import androidx.compose.material3.SnackbarDuration
|
||||
@ -11,26 +12,33 @@ import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.SnackbarResult
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.paging.LoadState
|
||||
import androidx.paging.compose.LazyPagingItems
|
||||
import eu.kanade.data.source.NoResultsException
|
||||
import eu.kanade.presentation.browse.components.BrowseSourceComfortableGrid
|
||||
import eu.kanade.presentation.browse.components.BrowseSourceCompactGrid
|
||||
import eu.kanade.presentation.browse.components.BrowseSourceList
|
||||
import eu.kanade.presentation.components.EmptyScreen
|
||||
import eu.kanade.presentation.components.EmptyScreenAction
|
||||
import eu.kanade.presentation.components.LoadingScreen
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.LocalSource
|
||||
import eu.kanade.presentation.components.AppBar
|
||||
import eu.kanade.presentation.util.formattedMessage
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import tachiyomi.core.i18n.stringResource
|
||||
import tachiyomi.domain.library.model.LibraryDisplayMode
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.domain.source.model.StubSource
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.material.Scaffold
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import tachiyomi.presentation.core.screens.EmptyScreen
|
||||
import tachiyomi.presentation.core.screens.EmptyScreenAction
|
||||
import tachiyomi.presentation.core.screens.LoadingScreen
|
||||
import tachiyomi.source.local.LocalSource
|
||||
|
||||
@Composable
|
||||
fun BrowseSourceContent(
|
||||
source: CatalogueSource?,
|
||||
source: Source?,
|
||||
mangaList: LazyPagingItems<StateFlow<Manga>>,
|
||||
columns: GridCells,
|
||||
displayMode: LibraryDisplayMode,
|
||||
@ -48,54 +56,50 @@ fun BrowseSourceContent(
|
||||
?: mangaList.loadState.append.takeIf { it is LoadState.Error }
|
||||
|
||||
val getErrorMessage: (LoadState.Error) -> String = { state ->
|
||||
when {
|
||||
state.error is NoResultsException -> context.getString(R.string.no_results_found)
|
||||
state.error.message.isNullOrEmpty() -> ""
|
||||
state.error.message.orEmpty().startsWith("HTTP error") -> "${state.error.message}: ${context.getString(R.string.http_error_hint)}"
|
||||
else -> state.error.message.orEmpty()
|
||||
}
|
||||
with(context) { state.error.formattedMessage }
|
||||
}
|
||||
|
||||
LaunchedEffect(errorState) {
|
||||
if (mangaList.itemCount > 0 && errorState != null && errorState is LoadState.Error) {
|
||||
val result = snackbarHostState.showSnackbar(
|
||||
message = getErrorMessage(errorState),
|
||||
actionLabel = context.getString(R.string.action_webview_refresh),
|
||||
actionLabel = context.stringResource(MR.strings.action_retry),
|
||||
duration = SnackbarDuration.Indefinite,
|
||||
)
|
||||
when (result) {
|
||||
SnackbarResult.Dismissed -> snackbarHostState.currentSnackbarData?.dismiss()
|
||||
SnackbarResult.ActionPerformed -> mangaList.refresh()
|
||||
SnackbarResult.ActionPerformed -> mangaList.retry()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (mangaList.itemCount <= 0 && errorState != null && errorState is LoadState.Error) {
|
||||
EmptyScreen(
|
||||
modifier = Modifier.padding(contentPadding),
|
||||
message = getErrorMessage(errorState),
|
||||
actions = if (source is LocalSource) {
|
||||
listOf(
|
||||
persistentListOf(
|
||||
EmptyScreenAction(
|
||||
stringResId = R.string.local_source_help_guide,
|
||||
icon = Icons.Outlined.HelpOutline,
|
||||
stringRes = MR.strings.local_source_help_guide,
|
||||
icon = Icons.AutoMirrored.Outlined.HelpOutline,
|
||||
onClick = onLocalSourceHelpClick,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
listOf(
|
||||
persistentListOf(
|
||||
EmptyScreenAction(
|
||||
stringResId = R.string.action_retry,
|
||||
stringRes = MR.strings.action_retry,
|
||||
icon = Icons.Outlined.Refresh,
|
||||
onClick = mangaList::refresh,
|
||||
),
|
||||
EmptyScreenAction(
|
||||
stringResId = R.string.action_open_in_web_view,
|
||||
stringRes = MR.strings.action_open_in_web_view,
|
||||
icon = Icons.Outlined.Public,
|
||||
onClick = onWebViewClick,
|
||||
),
|
||||
EmptyScreenAction(
|
||||
stringResId = R.string.label_help,
|
||||
icon = Icons.Outlined.HelpOutline,
|
||||
stringRes = MR.strings.label_help,
|
||||
icon = Icons.AutoMirrored.Outlined.HelpOutline,
|
||||
onClick = onHelpClick,
|
||||
),
|
||||
)
|
||||
@ -106,7 +110,9 @@ fun BrowseSourceContent(
|
||||
}
|
||||
|
||||
if (mangaList.itemCount == 0 && mangaList.loadState.refresh is LoadState.Loading) {
|
||||
LoadingScreen()
|
||||
LoadingScreen(
|
||||
modifier = Modifier.padding(contentPadding),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
@ -139,3 +145,24 @@ fun BrowseSourceContent(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun MissingSourceScreen(
|
||||
source: StubSource,
|
||||
navigateUp: () -> Unit,
|
||||
) {
|
||||
Scaffold(
|
||||
topBar = { scrollBehavior ->
|
||||
AppBar(
|
||||
title = source.name,
|
||||
navigateUp = navigateUp,
|
||||
scrollBehavior = scrollBehavior,
|
||||
)
|
||||
},
|
||||
) { paddingValues ->
|
||||
EmptyScreen(
|
||||
message = stringResource(MR.strings.source_not_installed, source.toString()),
|
||||
modifier = Modifier.padding(paddingValues),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -10,19 +10,17 @@ import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.HelpOutline
|
||||
import androidx.compose.material.icons.outlined.History
|
||||
import androidx.compose.material.icons.automirrored.outlined.Launch
|
||||
import androidx.compose.material.icons.outlined.Settings
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
@ -30,6 +28,7 @@ import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.VerticalDivider
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@ -38,7 +37,7 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
@ -47,75 +46,82 @@ import eu.kanade.domain.extension.interactor.ExtensionSourceItem
|
||||
import eu.kanade.presentation.browse.components.ExtensionIcon
|
||||
import eu.kanade.presentation.components.AppBar
|
||||
import eu.kanade.presentation.components.AppBarActions
|
||||
import eu.kanade.presentation.components.DIVIDER_ALPHA
|
||||
import eu.kanade.presentation.components.Divider
|
||||
import eu.kanade.presentation.components.EmptyScreen
|
||||
import eu.kanade.presentation.components.Scaffold
|
||||
import eu.kanade.presentation.components.ScrollbarLazyColumn
|
||||
import eu.kanade.presentation.components.WarningBanner
|
||||
import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget
|
||||
import eu.kanade.presentation.more.settings.widget.TrailingWidgetBuffer
|
||||
import eu.kanade.presentation.util.padding
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.extension.model.Extension
|
||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||
import eu.kanade.tachiyomi.ui.browse.extension.details.ExtensionDetailsState
|
||||
import eu.kanade.tachiyomi.ui.browse.extension.details.ExtensionDetailsScreenModel
|
||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.ScrollbarLazyColumn
|
||||
import tachiyomi.presentation.core.components.material.Scaffold
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import tachiyomi.presentation.core.screens.EmptyScreen
|
||||
|
||||
@Composable
|
||||
fun ExtensionDetailsScreen(
|
||||
navigateUp: () -> Unit,
|
||||
state: ExtensionDetailsState,
|
||||
state: ExtensionDetailsScreenModel.State,
|
||||
onClickSourcePreferences: (sourceId: Long) -> Unit,
|
||||
onClickWhatsNew: () -> Unit,
|
||||
onClickReadme: () -> Unit,
|
||||
onClickEnableAll: () -> Unit,
|
||||
onClickDisableAll: () -> Unit,
|
||||
onClickClearCookies: () -> Unit,
|
||||
onClickUninstall: () -> Unit,
|
||||
onClickSource: (sourceId: Long) -> Unit,
|
||||
) {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
val url = remember(state.extension) {
|
||||
val regex = """https://raw.githubusercontent.com/(.+?)/(.+?)/.+""".toRegex()
|
||||
regex.find(state.extension?.repoUrl.orEmpty())
|
||||
?.let {
|
||||
val (user, repo) = it.destructured
|
||||
"https://github.com/$user/$repo"
|
||||
}
|
||||
?: state.extension?.repoUrl
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = { scrollBehavior ->
|
||||
AppBar(
|
||||
title = stringResource(R.string.label_extension_info),
|
||||
title = stringResource(MR.strings.label_extension_info),
|
||||
navigateUp = navigateUp,
|
||||
actions = {
|
||||
AppBarActions(
|
||||
actions = buildList {
|
||||
if (state.extension?.isUnofficial == false) {
|
||||
actions = persistentListOf<AppBar.AppBarAction>().builder()
|
||||
.apply {
|
||||
if (url != null) {
|
||||
add(
|
||||
AppBar.Action(
|
||||
title = stringResource(R.string.whats_new),
|
||||
icon = Icons.Outlined.History,
|
||||
onClick = onClickWhatsNew,
|
||||
),
|
||||
)
|
||||
add(
|
||||
AppBar.Action(
|
||||
title = stringResource(R.string.action_faq_and_guides),
|
||||
icon = Icons.Outlined.HelpOutline,
|
||||
onClick = onClickReadme,
|
||||
title = stringResource(MR.strings.action_open_repo),
|
||||
icon = Icons.AutoMirrored.Outlined.Launch,
|
||||
onClick = {
|
||||
uriHandler.openUri(url)
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
addAll(
|
||||
listOf(
|
||||
AppBar.OverflowAction(
|
||||
title = stringResource(R.string.action_enable_all),
|
||||
title = stringResource(MR.strings.action_enable_all),
|
||||
onClick = onClickEnableAll,
|
||||
),
|
||||
AppBar.OverflowAction(
|
||||
title = stringResource(R.string.action_disable_all),
|
||||
title = stringResource(MR.strings.action_disable_all),
|
||||
onClick = onClickDisableAll,
|
||||
),
|
||||
AppBar.OverflowAction(
|
||||
title = stringResource(R.string.pref_clear_cookies),
|
||||
title = stringResource(MR.strings.pref_clear_cookies),
|
||||
onClick = onClickClearCookies,
|
||||
),
|
||||
),
|
||||
)
|
||||
},
|
||||
}
|
||||
.build(),
|
||||
)
|
||||
},
|
||||
scrollBehavior = scrollBehavior,
|
||||
@ -124,7 +130,7 @@ fun ExtensionDetailsScreen(
|
||||
) { paddingValues ->
|
||||
if (state.extension == null) {
|
||||
EmptyScreen(
|
||||
textResource = R.string.empty_screen,
|
||||
MR.strings.empty_screen,
|
||||
modifier = Modifier.padding(paddingValues),
|
||||
)
|
||||
return@Scaffold
|
||||
@ -145,7 +151,7 @@ fun ExtensionDetailsScreen(
|
||||
private fun ExtensionDetails(
|
||||
contentPadding: PaddingValues,
|
||||
extension: Extension.Installed,
|
||||
sources: List<ExtensionSourceItem>,
|
||||
sources: ImmutableList<ExtensionSourceItem>,
|
||||
onClickSourcePreferences: (sourceId: Long) -> Unit,
|
||||
onClickUninstall: () -> Unit,
|
||||
onClickSource: (sourceId: Long) -> Unit,
|
||||
@ -156,14 +162,9 @@ private fun ExtensionDetails(
|
||||
ScrollbarLazyColumn(
|
||||
contentPadding = contentPadding,
|
||||
) {
|
||||
when {
|
||||
extension.isUnofficial ->
|
||||
if (extension.isObsolete) {
|
||||
item {
|
||||
WarningBanner(R.string.unofficial_extension_message)
|
||||
}
|
||||
extension.isObsolete ->
|
||||
item {
|
||||
WarningBanner(R.string.obsolete_extension_message)
|
||||
WarningBanner(MR.strings.obsolete_extension_message)
|
||||
}
|
||||
}
|
||||
|
||||
@ -176,7 +177,8 @@ private fun ExtensionDetails(
|
||||
data = Uri.fromParts("package", extension.pkgName, null)
|
||||
context.startActivity(this)
|
||||
}
|
||||
},
|
||||
Unit
|
||||
}.takeIf { extension.isShared },
|
||||
onClickAgeRating = {
|
||||
showNsfwWarning = true
|
||||
},
|
||||
@ -209,7 +211,7 @@ private fun DetailsHeader(
|
||||
extension: Extension,
|
||||
onClickAgeRating: () -> Unit,
|
||||
onClickUninstall: () -> Unit,
|
||||
onClickAppInfo: () -> Unit,
|
||||
onClickAppInfo: (() -> Unit)?,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
@ -259,7 +261,7 @@ private fun DetailsHeader(
|
||||
InfoText(
|
||||
modifier = Modifier.weight(1f),
|
||||
primaryText = extension.versionName,
|
||||
secondaryText = stringResource(R.string.ext_info_version),
|
||||
secondaryText = stringResource(MR.strings.ext_info_version),
|
||||
)
|
||||
|
||||
InfoDivider()
|
||||
@ -267,7 +269,7 @@ private fun DetailsHeader(
|
||||
InfoText(
|
||||
modifier = Modifier.weight(if (extension.isNsfw) 1.5f else 1f),
|
||||
primaryText = LocaleHelper.getSourceDisplayName(extension.lang, context),
|
||||
secondaryText = stringResource(R.string.ext_info_language),
|
||||
secondaryText = stringResource(MR.strings.ext_info_language),
|
||||
)
|
||||
|
||||
if (extension.isNsfw) {
|
||||
@ -275,12 +277,12 @@ private fun DetailsHeader(
|
||||
|
||||
InfoText(
|
||||
modifier = Modifier.weight(1f),
|
||||
primaryText = stringResource(R.string.ext_nsfw_short),
|
||||
primaryText = stringResource(MR.strings.ext_nsfw_short),
|
||||
primaryTextStyle = MaterialTheme.typography.bodyLarge.copy(
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
fontWeight = FontWeight.Medium,
|
||||
),
|
||||
secondaryText = stringResource(R.string.ext_info_age_rating),
|
||||
secondaryText = stringResource(MR.strings.ext_info_age_rating),
|
||||
onClick = onClickAgeRating,
|
||||
)
|
||||
}
|
||||
@ -293,37 +295,38 @@ private fun DetailsHeader(
|
||||
top = MaterialTheme.padding.small,
|
||||
bottom = MaterialTheme.padding.medium,
|
||||
),
|
||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.medium),
|
||||
) {
|
||||
OutlinedButton(
|
||||
modifier = Modifier.weight(1f),
|
||||
onClick = onClickUninstall,
|
||||
) {
|
||||
Text(stringResource(R.string.ext_uninstall))
|
||||
Text(stringResource(MR.strings.ext_uninstall))
|
||||
}
|
||||
|
||||
Spacer(Modifier.width(16.dp))
|
||||
|
||||
if (onClickAppInfo != null) {
|
||||
Button(
|
||||
modifier = Modifier.weight(1f),
|
||||
onClick = onClickAppInfo,
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.ext_app_info),
|
||||
text = stringResource(MR.strings.ext_app_info),
|
||||
color = MaterialTheme.colorScheme.onPrimary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Divider()
|
||||
HorizontalDivider()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun InfoText(
|
||||
modifier: Modifier,
|
||||
primaryText: String,
|
||||
primaryTextStyle: TextStyle = MaterialTheme.typography.bodyLarge,
|
||||
secondaryText: String,
|
||||
modifier: Modifier = Modifier,
|
||||
primaryTextStyle: TextStyle = MaterialTheme.typography.bodyLarge,
|
||||
onClick: (() -> Unit)? = null,
|
||||
) {
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
@ -356,20 +359,17 @@ private fun InfoText(
|
||||
|
||||
@Composable
|
||||
private fun InfoDivider() {
|
||||
Divider(
|
||||
modifier = Modifier
|
||||
.height(20.dp)
|
||||
.width(1.dp),
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = DIVIDER_ALPHA),
|
||||
VerticalDivider(
|
||||
modifier = Modifier.height(20.dp),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SourceSwitchPreference(
|
||||
modifier: Modifier = Modifier,
|
||||
source: ExtensionSourceItem,
|
||||
onClickSourcePreferences: (sourceId: Long) -> Unit,
|
||||
onClickSource: (sourceId: Long) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
@ -388,7 +388,7 @@ private fun SourceSwitchPreference(
|
||||
IconButton(onClick = { onClickSourcePreferences(source.source.id) }) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Settings,
|
||||
contentDescription = stringResource(R.string.label_settings),
|
||||
contentDescription = stringResource(MR.strings.label_settings),
|
||||
tint = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
}
|
||||
@ -411,11 +411,11 @@ private fun NsfwWarningDialog(
|
||||
) {
|
||||
AlertDialog(
|
||||
text = {
|
||||
Text(text = stringResource(R.string.ext_nsfw_warning))
|
||||
Text(text = stringResource(MR.strings.ext_nsfw_warning))
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = onClickConfirm) {
|
||||
Text(text = stringResource(android.R.string.ok))
|
||||
Text(text = stringResource(MR.strings.action_ok))
|
||||
}
|
||||
},
|
||||
onDismissRequest = onClickConfirm,
|
||||
|
@ -2,19 +2,19 @@ package eu.kanade.presentation.browse
|
||||
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import eu.kanade.presentation.components.AppBar
|
||||
import eu.kanade.presentation.components.EmptyScreen
|
||||
import eu.kanade.presentation.components.FastScrollLazyColumn
|
||||
import eu.kanade.presentation.components.Scaffold
|
||||
import eu.kanade.presentation.more.settings.widget.SwitchPreferenceWidget
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionFilterState
|
||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.material.Scaffold
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import tachiyomi.presentation.core.screens.EmptyScreen
|
||||
|
||||
@Composable
|
||||
fun ExtensionFilterScreen(
|
||||
@ -25,7 +25,7 @@ fun ExtensionFilterScreen(
|
||||
Scaffold(
|
||||
topBar = { scrollBehavior ->
|
||||
AppBar(
|
||||
title = stringResource(R.string.label_extensions),
|
||||
title = stringResource(MR.strings.label_extensions),
|
||||
navigateUp = navigateUp,
|
||||
scrollBehavior = scrollBehavior,
|
||||
)
|
||||
@ -33,7 +33,7 @@ fun ExtensionFilterScreen(
|
||||
) { contentPadding ->
|
||||
if (state.isEmpty) {
|
||||
EmptyScreen(
|
||||
textResource = R.string.empty_screen,
|
||||
stringRes = MR.strings.empty_screen,
|
||||
modifier = Modifier.padding(contentPadding),
|
||||
)
|
||||
return@Scaffold
|
||||
@ -53,7 +53,7 @@ private fun ExtensionFilterContent(
|
||||
onClickLang: (String) -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
FastScrollLazyColumn(
|
||||
LazyColumn(
|
||||
contentPadding = contentPadding,
|
||||
) {
|
||||
items(state.languages) { language ->
|
||||
|
@ -1,10 +1,12 @@
|
||||
package eu.kanade.presentation.browse
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
@ -13,6 +15,11 @@ import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Close
|
||||
import androidx.compose.material.icons.outlined.GetApp
|
||||
import androidx.compose.material.icons.outlined.Public
|
||||
import androidx.compose.material.icons.outlined.Refresh
|
||||
import androidx.compose.material.icons.outlined.Settings
|
||||
import androidx.compose.material.icons.outlined.VerifiedUser
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
@ -31,36 +38,45 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.google.accompanist.flowlayout.FlowRow
|
||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import eu.kanade.presentation.browse.components.BaseBrowseItem
|
||||
import eu.kanade.presentation.browse.components.ExtensionIcon
|
||||
import eu.kanade.presentation.components.EmptyScreen
|
||||
import eu.kanade.presentation.components.FastScrollLazyColumn
|
||||
import eu.kanade.presentation.components.LoadingScreen
|
||||
import eu.kanade.presentation.components.PullRefresh
|
||||
import eu.kanade.presentation.components.WarningBanner
|
||||
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.presentation.more.settings.screen.browse.ExtensionReposScreen
|
||||
import eu.kanade.presentation.util.rememberRequestPackageInstallsPermissionState
|
||||
import eu.kanade.tachiyomi.extension.model.Extension
|
||||
import eu.kanade.tachiyomi.extension.model.InstallStep
|
||||
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionUiModel
|
||||
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionsState
|
||||
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionsScreenModel
|
||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
import eu.kanade.tachiyomi.util.system.launchRequestPackageInstallsPermission
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import tachiyomi.i18n.MR
|
||||
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.i18n.stringResource
|
||||
import tachiyomi.presentation.core.screens.EmptyScreen
|
||||
import tachiyomi.presentation.core.screens.EmptyScreenAction
|
||||
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
|
||||
fun ExtensionScreen(
|
||||
state: ExtensionsState,
|
||||
state: ExtensionsScreenModel.State,
|
||||
contentPadding: PaddingValues,
|
||||
searchQuery: String?,
|
||||
onLongClickItem: (Extension) -> Unit,
|
||||
onClickItemCancel: (Extension) -> Unit,
|
||||
onOpenWebView: (Extension.Available) -> Unit,
|
||||
onInstallExtension: (Extension.Available) -> Unit,
|
||||
onUninstallExtension: (Extension) -> Unit,
|
||||
onUpdateExtension: (Extension.Installed) -> Unit,
|
||||
@ -69,22 +85,31 @@ fun ExtensionScreen(
|
||||
onClickUpdateAll: () -> Unit,
|
||||
onRefresh: () -> Unit,
|
||||
) {
|
||||
val navigator = LocalNavigator.currentOrThrow
|
||||
|
||||
PullRefresh(
|
||||
refreshing = state.isRefreshing,
|
||||
onRefresh = onRefresh,
|
||||
enabled = !state.isLoading,
|
||||
enabled = { !state.isLoading },
|
||||
) {
|
||||
when {
|
||||
state.isLoading -> LoadingScreen(modifier = Modifier.padding(contentPadding))
|
||||
state.isLoading -> LoadingScreen(Modifier.padding(contentPadding))
|
||||
state.isEmpty -> {
|
||||
val msg = if (!searchQuery.isNullOrEmpty()) {
|
||||
R.string.no_results_found
|
||||
MR.strings.no_results_found
|
||||
} else {
|
||||
R.string.empty_screen
|
||||
MR.strings.empty_screen
|
||||
}
|
||||
EmptyScreen(
|
||||
textResource = msg,
|
||||
stringRes = msg,
|
||||
modifier = Modifier.padding(contentPadding),
|
||||
actions = persistentListOf(
|
||||
EmptyScreenAction(
|
||||
stringRes = MR.strings.label_extension_repos,
|
||||
icon = Icons.Outlined.Settings,
|
||||
onClick = { navigator.push(ExtensionReposScreen()) },
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
@ -93,6 +118,7 @@ fun ExtensionScreen(
|
||||
contentPadding = contentPadding,
|
||||
onLongClickItem = onLongClickItem,
|
||||
onClickItemCancel = onClickItemCancel,
|
||||
onOpenWebView = onOpenWebView,
|
||||
onInstallExtension = onInstallExtension,
|
||||
onUninstallExtension = onUninstallExtension,
|
||||
onUpdateExtension = onUpdateExtension,
|
||||
@ -107,10 +133,11 @@ fun ExtensionScreen(
|
||||
|
||||
@Composable
|
||||
private fun ExtensionContent(
|
||||
state: ExtensionsState,
|
||||
state: ExtensionsScreenModel.State,
|
||||
contentPadding: PaddingValues,
|
||||
onLongClickItem: (Extension) -> Unit,
|
||||
onClickItemCancel: (Extension) -> Unit,
|
||||
onOpenWebView: (Extension.Available) -> Unit,
|
||||
onInstallExtension: (Extension.Available) -> Unit,
|
||||
onUninstallExtension: (Extension) -> Unit,
|
||||
onUpdateExtension: (Extension.Installed) -> Unit,
|
||||
@ -118,11 +145,24 @@ private fun ExtensionContent(
|
||||
onOpenExtension: (Extension.Installed) -> Unit,
|
||||
onClickUpdateAll: () -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
var trustState by remember { mutableStateOf<Extension.Untrusted?>(null) }
|
||||
val installGranted = rememberRequestPackageInstallsPermissionState(initialValue = true)
|
||||
|
||||
FastScrollLazyColumn(
|
||||
contentPadding = contentPadding + topSmallPaddingValues,
|
||||
) {
|
||||
if (!installGranted && state.installer?.requiresSystemPermission == true) {
|
||||
item(key = "extension-permissions-warning") {
|
||||
WarningBanner(
|
||||
textRes = MR.strings.ext_permission_install_apps_warning,
|
||||
modifier = Modifier.clickable {
|
||||
context.launchRequestPackageInstallsPermission()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
state.items.forEach { (header, items) ->
|
||||
item(
|
||||
contentType = "header",
|
||||
@ -131,11 +171,11 @@ private fun ExtensionContent(
|
||||
when (header) {
|
||||
is ExtensionUiModel.Header.Resource -> {
|
||||
val action: @Composable RowScope.() -> Unit =
|
||||
if (header.textRes == R.string.ext_updates_pending) {
|
||||
if (header.textRes == MR.strings.ext_updates_pending) {
|
||||
{
|
||||
Button(onClick = { onClickUpdateAll() }) {
|
||||
Text(
|
||||
text = stringResource(R.string.ext_update_all),
|
||||
text = stringResource(MR.strings.ext_update_all),
|
||||
style = LocalTextStyle.current.copy(
|
||||
color = MaterialTheme.colorScheme.onPrimary,
|
||||
),
|
||||
@ -176,6 +216,13 @@ private fun ExtensionContent(
|
||||
}
|
||||
},
|
||||
onLongClickItem = onLongClickItem,
|
||||
onClickItemSecondaryAction = {
|
||||
when (it) {
|
||||
is Extension.Available -> onOpenWebView(it)
|
||||
is Extension.Installed -> onOpenExtension(it)
|
||||
else -> {}
|
||||
}
|
||||
},
|
||||
onClickItemCancel = onClickItemCancel,
|
||||
onClickItemAction = {
|
||||
when (it) {
|
||||
@ -213,12 +260,13 @@ private fun ExtensionContent(
|
||||
|
||||
@Composable
|
||||
private fun ExtensionItem(
|
||||
modifier: Modifier = Modifier,
|
||||
item: ExtensionUiModel.Item,
|
||||
onClickItem: (Extension) -> Unit,
|
||||
onLongClickItem: (Extension) -> Unit,
|
||||
onClickItemCancel: (Extension) -> Unit,
|
||||
onClickItemAction: (Extension) -> Unit,
|
||||
onClickItemSecondaryAction: (Extension) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val (extension, installStep) = item
|
||||
BaseBrowseItem(
|
||||
@ -243,7 +291,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(
|
||||
extension = extension,
|
||||
modifier = Modifier
|
||||
@ -258,6 +309,7 @@ private fun ExtensionItem(
|
||||
installStep = installStep,
|
||||
onClickItemCancel = onClickItemCancel,
|
||||
onClickItemAction = onClickItemAction,
|
||||
onClickItemSecondaryAction = onClickItemSecondaryAction,
|
||||
)
|
||||
},
|
||||
) {
|
||||
@ -287,7 +339,7 @@ private fun ExtensionItemContent(
|
||||
// Won't look good but it's not like we can ellipsize overflowing content
|
||||
FlowRow(
|
||||
modifier = Modifier.secondaryItemAlpha(),
|
||||
mainAxisSpacing = 4.dp,
|
||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
|
||||
) {
|
||||
ProvideTextStyle(value = MaterialTheme.typography.bodySmall) {
|
||||
if (extension is Extension.Installed && extension.lang.isNotEmpty()) {
|
||||
@ -303,10 +355,9 @@ private fun ExtensionItemContent(
|
||||
}
|
||||
|
||||
val warning = when {
|
||||
extension is Extension.Untrusted -> R.string.ext_untrusted
|
||||
extension is Extension.Installed && extension.isUnofficial -> R.string.ext_unofficial
|
||||
extension is Extension.Installed && extension.isObsolete -> R.string.ext_obsolete
|
||||
extension.isNsfw -> R.string.ext_nsfw_short
|
||||
extension is Extension.Untrusted -> MR.strings.ext_untrusted
|
||||
extension is Extension.Installed && extension.isObsolete -> MR.strings.ext_obsolete
|
||||
extension.isNsfw -> MR.strings.ext_nsfw_short
|
||||
else -> null
|
||||
}
|
||||
if (warning != null) {
|
||||
@ -322,9 +373,9 @@ private fun ExtensionItemContent(
|
||||
DotSeparatorNoSpaceText()
|
||||
Text(
|
||||
text = when (installStep) {
|
||||
InstallStep.Pending -> stringResource(R.string.ext_pending)
|
||||
InstallStep.Downloading -> stringResource(R.string.ext_downloading)
|
||||
InstallStep.Installing -> stringResource(R.string.ext_installing)
|
||||
InstallStep.Pending -> stringResource(MR.strings.ext_pending)
|
||||
InstallStep.Downloading -> stringResource(MR.strings.ext_downloading)
|
||||
InstallStep.Installing -> stringResource(MR.strings.ext_installing)
|
||||
else -> error("Must not show non-install process text")
|
||||
},
|
||||
)
|
||||
@ -341,48 +392,86 @@ private fun ExtensionItemActions(
|
||||
modifier: Modifier = Modifier,
|
||||
onClickItemCancel: (Extension) -> Unit = {},
|
||||
onClickItemAction: (Extension) -> Unit = {},
|
||||
onClickItemSecondaryAction: (Extension) -> Unit = {},
|
||||
) {
|
||||
val isIdle = installStep.isCompleted()
|
||||
Row(modifier = modifier) {
|
||||
if (isIdle) {
|
||||
TextButton(
|
||||
onClick = { onClickItemAction(extension) },
|
||||
|
||||
Row(
|
||||
modifier = modifier,
|
||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
||||
) {
|
||||
Text(
|
||||
text = when (installStep) {
|
||||
InstallStep.Installed -> stringResource(R.string.ext_installed)
|
||||
InstallStep.Error -> stringResource(R.string.action_retry)
|
||||
InstallStep.Idle -> {
|
||||
when (extension) {
|
||||
is Extension.Installed -> {
|
||||
if (extension.hasUpdate) {
|
||||
stringResource(R.string.ext_update)
|
||||
} else {
|
||||
stringResource(R.string.action_settings)
|
||||
}
|
||||
}
|
||||
is Extension.Untrusted -> stringResource(R.string.ext_trust)
|
||||
is Extension.Available -> stringResource(R.string.ext_install)
|
||||
}
|
||||
}
|
||||
else -> error("Must not show install process text")
|
||||
},
|
||||
)
|
||||
}
|
||||
} else {
|
||||
when {
|
||||
!isIdle -> {
|
||||
IconButton(onClick = { onClickItemCancel(extension) }) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Close,
|
||||
contentDescription = stringResource(R.string.action_cancel),
|
||||
contentDescription = stringResource(MR.strings.action_cancel),
|
||||
)
|
||||
}
|
||||
}
|
||||
installStep == InstallStep.Error -> {
|
||||
IconButton(onClick = { onClickItemAction(extension) }) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Refresh,
|
||||
contentDescription = stringResource(MR.strings.action_retry),
|
||||
)
|
||||
}
|
||||
}
|
||||
installStep == InstallStep.Idle -> {
|
||||
when (extension) {
|
||||
is Extension.Installed -> {
|
||||
IconButton(onClick = { onClickItemSecondaryAction(extension) }) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Settings,
|
||||
contentDescription = stringResource(MR.strings.action_settings),
|
||||
)
|
||||
}
|
||||
|
||||
if (extension.hasUpdate) {
|
||||
IconButton(onClick = { onClickItemAction(extension) }) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.GetApp,
|
||||
contentDescription = stringResource(MR.strings.ext_update),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
is Extension.Untrusted -> {
|
||||
IconButton(onClick = { onClickItemAction(extension) }) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.VerifiedUser,
|
||||
contentDescription = stringResource(MR.strings.ext_trust),
|
||||
)
|
||||
}
|
||||
}
|
||||
is Extension.Available -> {
|
||||
if (extension.sources.isNotEmpty()) {
|
||||
IconButton(
|
||||
onClick = { onClickItemSecondaryAction(extension) },
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Public,
|
||||
contentDescription = stringResource(MR.strings.action_open_in_web_view),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
IconButton(onClick = { onClickItemAction(extension) }) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.GetApp,
|
||||
contentDescription = stringResource(MR.strings.ext_install),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ExtensionHeader(
|
||||
@StringRes textRes: Int,
|
||||
textRes: StringResource,
|
||||
modifier: Modifier = Modifier,
|
||||
action: @Composable RowScope.() -> Unit = {},
|
||||
) {
|
||||
@ -422,19 +511,19 @@ private fun ExtensionTrustDialog(
|
||||
) {
|
||||
AlertDialog(
|
||||
title = {
|
||||
Text(text = stringResource(R.string.untrusted_extension))
|
||||
Text(text = stringResource(MR.strings.untrusted_extension))
|
||||
},
|
||||
text = {
|
||||
Text(text = stringResource(R.string.untrusted_extension_message))
|
||||
Text(text = stringResource(MR.strings.untrusted_extension_message))
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = onClickConfirm) {
|
||||
Text(text = stringResource(R.string.ext_trust))
|
||||
Text(text = stringResource(MR.strings.ext_trust))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onClickDismiss) {
|
||||
Text(text = stringResource(R.string.ext_uninstall))
|
||||
Text(text = stringResource(MR.strings.ext_uninstall))
|
||||
}
|
||||
},
|
||||
onDismissRequest = onDismissRequest,
|
||||
|
@ -1,35 +1,31 @@
|
||||
package eu.kanade.presentation.browse
|
||||
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import eu.kanade.presentation.browse.components.GlobalSearchCardRow
|
||||
import eu.kanade.presentation.browse.components.GlobalSearchErrorResultItem
|
||||
import eu.kanade.presentation.browse.components.GlobalSearchLoadingResultItem
|
||||
import eu.kanade.presentation.browse.components.GlobalSearchResultItem
|
||||
import eu.kanade.presentation.browse.components.GlobalSearchToolbar
|
||||
import eu.kanade.presentation.components.LazyColumn
|
||||
import eu.kanade.presentation.components.Scaffold
|
||||
import eu.kanade.presentation.util.padding
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchState
|
||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchItemResult
|
||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchScreenModel
|
||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SourceFilter
|
||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.presentation.core.components.material.Scaffold
|
||||
|
||||
@Composable
|
||||
fun GlobalSearchScreen(
|
||||
state: GlobalSearchState,
|
||||
state: SearchScreenModel.State,
|
||||
navigateUp: () -> Unit,
|
||||
onChangeSearchQuery: (String?) -> Unit,
|
||||
onSearch: (String) -> Unit,
|
||||
getManga: @Composable (CatalogueSource, Manga) -> State<Manga>,
|
||||
onChangeSearchFilter: (SourceFilter) -> Unit,
|
||||
onToggleResults: () -> Unit,
|
||||
getManga: @Composable (Manga) -> State<Manga>,
|
||||
onClickSource: (CatalogueSource) -> Unit,
|
||||
onClickItem: (Manga) -> Unit,
|
||||
onLongClickItem: (Manga) -> Unit,
|
||||
@ -43,12 +39,16 @@ fun GlobalSearchScreen(
|
||||
navigateUp = navigateUp,
|
||||
onChangeSearchQuery = onChangeSearchQuery,
|
||||
onSearch = onSearch,
|
||||
sourceFilter = state.sourceFilter,
|
||||
onChangeSearchFilter = onChangeSearchFilter,
|
||||
onlyShowHasResults = state.onlyShowHasResults,
|
||||
onToggleResults = onToggleResults,
|
||||
scrollBehavior = scrollBehavior,
|
||||
)
|
||||
},
|
||||
) { paddingValues ->
|
||||
GlobalSearchContent(
|
||||
items = state.items,
|
||||
items = state.filteredItems,
|
||||
contentPadding = paddingValues,
|
||||
getManga = getManga,
|
||||
onClickSource = onClickSource,
|
||||
@ -59,13 +59,14 @@ fun GlobalSearchScreen(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun GlobalSearchContent(
|
||||
internal fun GlobalSearchContent(
|
||||
items: Map<CatalogueSource, SearchItemResult>,
|
||||
contentPadding: PaddingValues,
|
||||
getManga: @Composable (CatalogueSource, Manga) -> State<Manga>,
|
||||
getManga: @Composable (Manga) -> State<Manga>,
|
||||
onClickSource: (CatalogueSource) -> Unit,
|
||||
onClickItem: (Manga) -> Unit,
|
||||
onLongClickItem: (Manga) -> Unit,
|
||||
fromSourceId: Long? = null,
|
||||
) {
|
||||
LazyColumn(
|
||||
contentPadding = contentPadding,
|
||||
@ -73,8 +74,10 @@ fun GlobalSearchContent(
|
||||
items.forEach { (source, result) ->
|
||||
item(key = source.id) {
|
||||
GlobalSearchResultItem(
|
||||
title = source.name,
|
||||
subtitle = LocaleHelper.getDisplayName(source.lang),
|
||||
title = fromSourceId?.let {
|
||||
"▶ ${source.name}".takeIf { source.id == fromSourceId }
|
||||
} ?: source.name,
|
||||
subtitle = LocaleHelper.getLocalizedDisplayName(source.lang),
|
||||
onClick = { onClickSource(source) },
|
||||
) {
|
||||
when (result) {
|
||||
@ -82,21 +85,9 @@ fun GlobalSearchContent(
|
||||
GlobalSearchLoadingResultItem()
|
||||
}
|
||||
is SearchItemResult.Success -> {
|
||||
if (result.isEmpty) {
|
||||
Text(
|
||||
text = stringResource(R.string.no_results_found),
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
horizontal = MaterialTheme.padding.medium,
|
||||
vertical = MaterialTheme.padding.small,
|
||||
),
|
||||
)
|
||||
return@GlobalSearchResultItem
|
||||
}
|
||||
|
||||
GlobalSearchCardRow(
|
||||
titles = result.result,
|
||||
getManga = { getManga(source, it) },
|
||||
getManga = getManga,
|
||||
onClick = onClickItem,
|
||||
onLongClick = onLongClickItem,
|
||||
)
|
||||
|
@ -6,19 +6,19 @@ import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import eu.kanade.presentation.components.AppBar
|
||||
import eu.kanade.presentation.components.EmptyScreen
|
||||
import eu.kanade.presentation.components.FastScrollLazyColumn
|
||||
import eu.kanade.presentation.components.Scaffold
|
||||
import eu.kanade.presentation.manga.components.BaseMangaListItem
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.ui.browse.migration.manga.MigrateMangaState
|
||||
import eu.kanade.tachiyomi.ui.browse.migration.manga.MigrateMangaScreenModel
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.FastScrollLazyColumn
|
||||
import tachiyomi.presentation.core.components.material.Scaffold
|
||||
import tachiyomi.presentation.core.screens.EmptyScreen
|
||||
|
||||
@Composable
|
||||
fun MigrateMangaScreen(
|
||||
navigateUp: () -> Unit,
|
||||
title: String?,
|
||||
state: MigrateMangaState,
|
||||
state: MigrateMangaScreenModel.State,
|
||||
onClickItem: (Manga) -> Unit,
|
||||
onClickCover: (Manga) -> Unit,
|
||||
) {
|
||||
@ -33,7 +33,7 @@ fun MigrateMangaScreen(
|
||||
) { contentPadding ->
|
||||
if (state.isEmpty) {
|
||||
EmptyScreen(
|
||||
textResource = R.string.empty_screen,
|
||||
stringRes = MR.strings.empty_screen,
|
||||
modifier = Modifier.padding(contentPadding),
|
||||
)
|
||||
return@Scaffold
|
||||
@ -51,7 +51,7 @@ fun MigrateMangaScreen(
|
||||
@Composable
|
||||
private fun MigrateMangaContent(
|
||||
contentPadding: PaddingValues,
|
||||
state: MigrateMangaState,
|
||||
state: MigrateMangaScreenModel.State,
|
||||
onClickItem: (Manga) -> Unit,
|
||||
onClickCover: (Manga) -> Unit,
|
||||
) {
|
||||
@ -70,10 +70,10 @@ private fun MigrateMangaContent(
|
||||
|
||||
@Composable
|
||||
private fun MigrateMangaItem(
|
||||
modifier: Modifier = Modifier,
|
||||
manga: Manga,
|
||||
onClickItem: (Manga) -> Unit,
|
||||
onClickCover: (Manga) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
BaseMangaListItem(
|
||||
modifier = modifier,
|
||||
|
@ -1,29 +1,24 @@
|
||||
package eu.kanade.presentation.browse
|
||||
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import eu.kanade.presentation.browse.components.GlobalSearchCardRow
|
||||
import eu.kanade.presentation.browse.components.GlobalSearchEmptyResultItem
|
||||
import eu.kanade.presentation.browse.components.GlobalSearchErrorResultItem
|
||||
import eu.kanade.presentation.browse.components.GlobalSearchLoadingResultItem
|
||||
import eu.kanade.presentation.browse.components.GlobalSearchResultItem
|
||||
import eu.kanade.presentation.browse.components.GlobalSearchToolbar
|
||||
import eu.kanade.presentation.components.LazyColumn
|
||||
import eu.kanade.presentation.components.Scaffold
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.ui.browse.migration.search.MigrateSearchState
|
||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchItemResult
|
||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchScreenModel
|
||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SourceFilter
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.presentation.core.components.material.Scaffold
|
||||
|
||||
@Composable
|
||||
fun MigrateSearchScreen(
|
||||
state: SearchScreenModel.State,
|
||||
fromSourceId: Long?,
|
||||
navigateUp: () -> Unit,
|
||||
state: MigrateSearchState,
|
||||
getManga: @Composable (CatalogueSource, Manga) -> State<Manga>,
|
||||
onChangeSearchQuery: (String?) -> Unit,
|
||||
onSearch: (String) -> Unit,
|
||||
onChangeSearchFilter: (SourceFilter) -> Unit,
|
||||
onToggleResults: () -> Unit,
|
||||
getManga: @Composable (Manga) -> State<Manga>,
|
||||
onClickSource: (CatalogueSource) -> Unit,
|
||||
onClickItem: (Manga) -> Unit,
|
||||
onLongClickItem: (Manga) -> Unit,
|
||||
@ -37,13 +32,17 @@ fun MigrateSearchScreen(
|
||||
navigateUp = navigateUp,
|
||||
onChangeSearchQuery = onChangeSearchQuery,
|
||||
onSearch = onSearch,
|
||||
sourceFilter = state.sourceFilter,
|
||||
onChangeSearchFilter = onChangeSearchFilter,
|
||||
onlyShowHasResults = state.onlyShowHasResults,
|
||||
onToggleResults = onToggleResults,
|
||||
scrollBehavior = scrollBehavior,
|
||||
)
|
||||
},
|
||||
) { paddingValues ->
|
||||
MigrateSearchContent(
|
||||
sourceId = state.manga?.source ?: -1,
|
||||
items = state.items,
|
||||
GlobalSearchContent(
|
||||
fromSourceId = fromSourceId,
|
||||
items = state.filteredItems,
|
||||
contentPadding = paddingValues,
|
||||
getManga = getManga,
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -20,30 +20,31 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import eu.kanade.domain.source.interactor.SetMigrateSorting
|
||||
import eu.kanade.presentation.browse.components.BaseSourceItem
|
||||
import eu.kanade.presentation.browse.components.SourceIcon
|
||||
import eu.kanade.presentation.components.Badge
|
||||
import eu.kanade.presentation.components.BadgeGroup
|
||||
import eu.kanade.presentation.components.EmptyScreen
|
||||
import eu.kanade.presentation.components.LoadingScreen
|
||||
import eu.kanade.presentation.components.ScrollbarLazyColumn
|
||||
import eu.kanade.presentation.components.Scroller.STICKY_HEADER_KEY_PREFIX
|
||||
import eu.kanade.presentation.theme.header
|
||||
import eu.kanade.presentation.util.padding
|
||||
import eu.kanade.presentation.util.plus
|
||||
import eu.kanade.presentation.util.secondaryItemAlpha
|
||||
import eu.kanade.presentation.util.topSmallPaddingValues
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.ui.browse.migration.sources.MigrateSourceState
|
||||
import eu.kanade.tachiyomi.ui.browse.migration.sources.MigrateSourceScreenModel
|
||||
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import tachiyomi.domain.source.model.Source
|
||||
import tachiyomi.i18n.MR
|
||||
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.i18n.stringResource
|
||||
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
|
||||
fun MigrateSourceScreen(
|
||||
state: MigrateSourceState,
|
||||
state: MigrateSourceScreenModel.State,
|
||||
contentPadding: PaddingValues,
|
||||
onClickItem: (Source) -> Unit,
|
||||
onToggleSortingDirection: () -> Unit,
|
||||
@ -51,9 +52,9 @@ fun MigrateSourceScreen(
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
when {
|
||||
state.isLoading -> LoadingScreen(modifier = Modifier.padding(contentPadding))
|
||||
state.isLoading -> LoadingScreen(Modifier.padding(contentPadding))
|
||||
state.isEmpty -> EmptyScreen(
|
||||
textResource = R.string.information_empty_library,
|
||||
stringRes = MR.strings.information_empty_library,
|
||||
modifier = Modifier.padding(contentPadding),
|
||||
)
|
||||
else ->
|
||||
@ -75,7 +76,7 @@ fun MigrateSourceScreen(
|
||||
|
||||
@Composable
|
||||
private fun MigrateSourceList(
|
||||
list: List<Pair<Source, Long>>,
|
||||
list: ImmutableList<Pair<Source, Long>>,
|
||||
contentPadding: PaddingValues,
|
||||
onClickItem: (Source) -> Unit,
|
||||
onLongClickItem: (Source) -> Unit,
|
||||
@ -95,21 +96,33 @@ private fun MigrateSourceList(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.migration_selection_prompt),
|
||||
text = stringResource(MR.strings.migration_selection_prompt),
|
||||
modifier = Modifier.weight(1f),
|
||||
style = MaterialTheme.typography.header,
|
||||
)
|
||||
|
||||
IconButton(onClick = onToggleSortingMode) {
|
||||
when (sortingMode) {
|
||||
SetMigrateSorting.Mode.ALPHABETICAL -> Icon(Icons.Outlined.SortByAlpha, contentDescription = stringResource(R.string.action_sort_alpha))
|
||||
SetMigrateSorting.Mode.TOTAL -> Icon(Icons.Outlined.Numbers, contentDescription = stringResource(R.string.action_sort_count))
|
||||
SetMigrateSorting.Mode.ALPHABETICAL -> Icon(
|
||||
Icons.Outlined.SortByAlpha,
|
||||
contentDescription = stringResource(MR.strings.action_sort_alpha),
|
||||
)
|
||||
SetMigrateSorting.Mode.TOTAL -> Icon(
|
||||
Icons.Outlined.Numbers,
|
||||
contentDescription = stringResource(MR.strings.action_sort_count),
|
||||
)
|
||||
}
|
||||
}
|
||||
IconButton(onClick = onToggleSortingDirection) {
|
||||
when (sortingDirection) {
|
||||
SetMigrateSorting.Direction.ASCENDING -> Icon(Icons.Outlined.ArrowUpward, contentDescription = stringResource(R.string.action_asc))
|
||||
SetMigrateSorting.Direction.DESCENDING -> Icon(Icons.Outlined.ArrowDownward, contentDescription = stringResource(R.string.action_desc))
|
||||
SetMigrateSorting.Direction.ASCENDING -> Icon(
|
||||
Icons.Outlined.ArrowUpward,
|
||||
contentDescription = stringResource(MR.strings.action_asc),
|
||||
)
|
||||
SetMigrateSorting.Direction.DESCENDING -> Icon(
|
||||
Icons.Outlined.ArrowDownward,
|
||||
contentDescription = stringResource(MR.strings.action_desc),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -132,11 +145,11 @@ private fun MigrateSourceList(
|
||||
|
||||
@Composable
|
||||
private fun MigrateSourceItem(
|
||||
modifier: Modifier = Modifier,
|
||||
source: Source,
|
||||
count: Long,
|
||||
onClickItem: () -> Unit,
|
||||
onLongClickItem: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
BaseSourceItem(
|
||||
modifier = modifier,
|
||||
@ -178,7 +191,7 @@ private fun MigrateSourceItem(
|
||||
if (source.isStub) {
|
||||
Text(
|
||||
modifier = Modifier.secondaryItemAlpha(),
|
||||
text = stringResource(R.string.not_installed),
|
||||
text = stringResource(MR.strings.not_installed),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
|
@ -7,29 +7,29 @@ import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import eu.kanade.presentation.browse.components.BaseSourceItem
|
||||
import eu.kanade.presentation.components.AppBar
|
||||
import eu.kanade.presentation.components.EmptyScreen
|
||||
import eu.kanade.presentation.components.FastScrollLazyColumn
|
||||
import eu.kanade.presentation.components.Scaffold
|
||||
import eu.kanade.presentation.more.settings.widget.SwitchPreferenceWidget
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.ui.browse.source.SourcesFilterState
|
||||
import eu.kanade.tachiyomi.ui.browse.source.SourcesFilterScreenModel
|
||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
import tachiyomi.domain.source.model.Source
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.FastScrollLazyColumn
|
||||
import tachiyomi.presentation.core.components.material.Scaffold
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import tachiyomi.presentation.core.screens.EmptyScreen
|
||||
|
||||
@Composable
|
||||
fun SourcesFilterScreen(
|
||||
navigateUp: () -> Unit,
|
||||
state: SourcesFilterState.Success,
|
||||
state: SourcesFilterScreenModel.State.Success,
|
||||
onClickLanguage: (String) -> Unit,
|
||||
onClickSource: (Source) -> Unit,
|
||||
) {
|
||||
Scaffold(
|
||||
topBar = { scrollBehavior ->
|
||||
AppBar(
|
||||
title = stringResource(R.string.label_sources),
|
||||
title = stringResource(MR.strings.label_sources),
|
||||
navigateUp = navigateUp,
|
||||
scrollBehavior = scrollBehavior,
|
||||
)
|
||||
@ -37,7 +37,7 @@ fun SourcesFilterScreen(
|
||||
) { contentPadding ->
|
||||
if (state.isEmpty) {
|
||||
EmptyScreen(
|
||||
textResource = R.string.source_filter_empty_screen,
|
||||
stringRes = MR.strings.source_filter_empty_screen,
|
||||
modifier = Modifier.padding(contentPadding),
|
||||
)
|
||||
return@Scaffold
|
||||
@ -54,7 +54,7 @@ fun SourcesFilterScreen(
|
||||
@Composable
|
||||
private fun SourcesFilterContent(
|
||||
contentPadding: PaddingValues,
|
||||
state: SourcesFilterState.Success,
|
||||
state: SourcesFilterScreenModel.State.Success,
|
||||
onClickLanguage: (String) -> Unit,
|
||||
onClickSource: (Source) -> Unit,
|
||||
) {
|
||||
@ -64,7 +64,7 @@ private fun SourcesFilterContent(
|
||||
state.items.forEach { (language, sources) ->
|
||||
val enabled = language in state.enabledLanguages
|
||||
item(
|
||||
key = language.hashCode(),
|
||||
key = language,
|
||||
contentType = "source-filter-header",
|
||||
) {
|
||||
SourcesFilterHeader(
|
||||
@ -74,7 +74,7 @@ private fun SourcesFilterContent(
|
||||
onClickItem = onClickLanguage,
|
||||
)
|
||||
}
|
||||
if (!enabled) return@forEach
|
||||
if (enabled) {
|
||||
items(
|
||||
items = sources,
|
||||
key = { "source-filter-${it.key()}" },
|
||||
@ -90,13 +90,14 @@ private fun SourcesFilterContent(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SourcesFilterHeader(
|
||||
modifier: Modifier,
|
||||
language: String,
|
||||
enabled: Boolean,
|
||||
onClickItem: (String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
SwitchPreferenceWidget(
|
||||
modifier = modifier,
|
||||
@ -108,10 +109,10 @@ private fun SourcesFilterHeader(
|
||||
|
||||
@Composable
|
||||
private fun SourcesFilterItem(
|
||||
modifier: Modifier,
|
||||
source: Source,
|
||||
enabled: Boolean,
|
||||
onClickItem: (Source) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
BaseSourceItem(
|
||||
modifier = modifier,
|
||||
|
@ -19,36 +19,37 @@ import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
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.source.LocalSource
|
||||
import eu.kanade.tachiyomi.ui.browse.source.SourcesState
|
||||
import eu.kanade.tachiyomi.ui.browse.source.SourcesScreenModel
|
||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreenModel.Listing
|
||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
import tachiyomi.domain.source.model.Pin
|
||||
import tachiyomi.domain.source.model.Source
|
||||
import tachiyomi.i18n.MR
|
||||
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.i18n.stringResource
|
||||
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
|
||||
fun SourcesScreen(
|
||||
state: SourcesState,
|
||||
state: SourcesScreenModel.State,
|
||||
contentPadding: PaddingValues,
|
||||
onClickItem: (Source, Listing) -> Unit,
|
||||
onClickPin: (Source) -> Unit,
|
||||
onLongClickItem: (Source) -> Unit,
|
||||
) {
|
||||
when {
|
||||
state.isLoading -> LoadingScreen(modifier = Modifier.padding(contentPadding))
|
||||
state.isLoading -> LoadingScreen(Modifier.padding(contentPadding))
|
||||
state.isEmpty -> EmptyScreen(
|
||||
textResource = R.string.source_empty_screen,
|
||||
stringRes = MR.strings.source_empty_screen,
|
||||
modifier = Modifier.padding(contentPadding),
|
||||
)
|
||||
else -> {
|
||||
@ -93,8 +94,8 @@ fun SourcesScreen(
|
||||
|
||||
@Composable
|
||||
private fun SourceHeader(
|
||||
modifier: Modifier = Modifier,
|
||||
language: String,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
Text(
|
||||
@ -107,11 +108,11 @@ private fun SourceHeader(
|
||||
|
||||
@Composable
|
||||
private fun SourceItem(
|
||||
modifier: Modifier = Modifier,
|
||||
source: Source,
|
||||
onClickItem: (Source, Listing) -> Unit,
|
||||
onLongClickItem: (Source) -> Unit,
|
||||
onClickPin: (Source) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
BaseSourceItem(
|
||||
modifier = modifier,
|
||||
@ -122,7 +123,7 @@ private fun SourceItem(
|
||||
if (source.supportsLatest) {
|
||||
TextButton(onClick = { onClickItem(source, Listing.Latest) }) {
|
||||
Text(
|
||||
text = stringResource(R.string.latest),
|
||||
text = stringResource(MR.strings.latest),
|
||||
style = LocalTextStyle.current.copy(
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
),
|
||||
@ -143,8 +144,14 @@ private fun SourcePinButton(
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
val icon = if (isPinned) Icons.Filled.PushPin else Icons.Outlined.PushPin
|
||||
val tint = if (isPinned) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onBackground
|
||||
val description = if (isPinned) R.string.action_unpin else R.string.action_pin
|
||||
val tint = if (isPinned) {
|
||||
MaterialTheme.colorScheme.primary
|
||||
} else {
|
||||
MaterialTheme.colorScheme.onBackground.copy(
|
||||
alpha = SecondaryItemAlpha,
|
||||
)
|
||||
}
|
||||
val description = if (isPinned) MR.strings.action_unpin else MR.strings.action_pin
|
||||
IconButton(onClick = onClick) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
@ -167,7 +174,7 @@ fun SourceOptionsDialog(
|
||||
},
|
||||
text = {
|
||||
Column {
|
||||
val textId = if (Pin.Pinned in source.pin) R.string.action_unpin else R.string.action_pin
|
||||
val textId = if (Pin.Pinned in source.pin) MR.strings.action_unpin else MR.strings.action_pin
|
||||
Text(
|
||||
text = stringResource(textId),
|
||||
modifier = Modifier
|
||||
@ -175,9 +182,9 @@ fun SourceOptionsDialog(
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 16.dp),
|
||||
)
|
||||
if (source.id != LocalSource.ID) {
|
||||
if (!source.isLocal()) {
|
||||
Text(
|
||||
text = stringResource(R.string.action_disable),
|
||||
text = stringResource(MR.strings.action_disable),
|
||||
modifier = Modifier
|
||||
.clickable(onClick = onClickDisable)
|
||||
.fillMaxWidth()
|
||||
@ -191,7 +198,7 @@ fun SourceOptionsDialog(
|
||||
)
|
||||
}
|
||||
|
||||
sealed class SourceUiModel {
|
||||
data class Item(val source: Source) : SourceUiModel()
|
||||
data class Header(val language: String) : SourceUiModel()
|
||||
sealed interface SourceUiModel {
|
||||
data class Item(val source: Source) : 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.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import eu.kanade.presentation.util.padding
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
|
||||
@Composable
|
||||
fun BaseBrowseItem(
|
||||
|
@ -9,15 +9,15 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
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 tachiyomi.domain.source.model.Source
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
import tachiyomi.presentation.core.util.secondaryItemAlpha
|
||||
|
||||
@Composable
|
||||
fun BaseSourceItem(
|
||||
modifier: Modifier = Modifier,
|
||||
source: Source,
|
||||
modifier: Modifier = Modifier,
|
||||
showLanguageInContent: Boolean = true,
|
||||
onClickItem: () -> Unit = {},
|
||||
onLongClickItem: () -> Unit = {},
|
||||
@ -25,7 +25,9 @@ fun BaseSourceItem(
|
||||
action: @Composable RowScope.(Source) -> Unit = {},
|
||||
content: @Composable RowScope.(Source, String?) -> Unit = defaultContent,
|
||||
) {
|
||||
val sourceLangString = LocaleHelper.getSourceDisplayName(source.lang, LocalContext.current).takeIf { showLanguageInContent }
|
||||
val sourceLangString = LocaleHelper.getSourceDisplayName(source.lang, LocalContext.current).takeIf {
|
||||
showLanguageInContent
|
||||
}
|
||||
BaseBrowseItem(
|
||||
modifier = modifier,
|
||||
onClickItem = onClickItem,
|
||||
|
@ -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
|
||||
|
||||
import android.content.pm.PackageManager
|
||||
import android.util.DisplayMetrics
|
||||
import androidx.compose.foundation.Image
|
||||
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.tachiyomi.R
|
||||
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.domain.source.model.Source
|
||||
import tachiyomi.source.local.isLocal
|
||||
|
||||
private val defaultModifier = Modifier
|
||||
.height(40.dp)
|
||||
@ -62,7 +62,7 @@ fun SourceIcon(
|
||||
modifier = modifier.then(defaultModifier),
|
||||
)
|
||||
}
|
||||
source.id == LocalSource.ID -> {
|
||||
source.isLocal() -> {
|
||||
Image(
|
||||
painter = painterResource(R.mipmap.ic_local_source),
|
||||
contentDescription = null,
|
||||
@ -127,7 +127,7 @@ private fun Extension.getIcon(density: Int = DisplayMetrics.DENSITY_DEFAULT): St
|
||||
return produceState<Result<ImageBitmap>>(initialValue = Result.Loading, this) {
|
||||
withIOContext {
|
||||
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)
|
||||
Result.Success(
|
||||
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> {
|
||||
object Loading : Result<Nothing>()
|
||||
object Error : Result<Nothing>()
|
||||
data object Loading : Result<Nothing>()
|
||||
data object Error : Result<Nothing>()
|
||||
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.paging.LoadState
|
||||
import androidx.paging.compose.LazyPagingItems
|
||||
import eu.kanade.presentation.browse.InLibraryBadge
|
||||
import eu.kanade.presentation.components.CommonMangaItemDefaults
|
||||
import eu.kanade.presentation.components.MangaComfortableGridItem
|
||||
import eu.kanade.presentation.util.plus
|
||||
import eu.kanade.presentation.library.components.CommonMangaItemDefaults
|
||||
import eu.kanade.presentation.library.components.MangaComfortableGridItem
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.domain.manga.model.MangaCover
|
||||
import tachiyomi.presentation.core.util.plus
|
||||
|
||||
@Composable
|
||||
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
|
||||
BrowseSourceComfortableGridItem(
|
||||
manga = manga,
|
||||
@ -57,7 +56,7 @@ fun BrowseSourceComfortableGrid(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BrowseSourceComfortableGridItem(
|
||||
private fun BrowseSourceComfortableGridItem(
|
||||
manga: Manga,
|
||||
onClick: () -> Unit = {},
|
||||
onLongClick: () -> Unit = onClick,
|
||||
|
@ -11,13 +11,12 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.paging.LoadState
|
||||
import androidx.paging.compose.LazyPagingItems
|
||||
import eu.kanade.presentation.browse.InLibraryBadge
|
||||
import eu.kanade.presentation.components.CommonMangaItemDefaults
|
||||
import eu.kanade.presentation.components.MangaCompactGridItem
|
||||
import eu.kanade.presentation.util.plus
|
||||
import eu.kanade.presentation.library.components.CommonMangaItemDefaults
|
||||
import eu.kanade.presentation.library.components.MangaCompactGridItem
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.domain.manga.model.MangaCover
|
||||
import tachiyomi.presentation.core.util.plus
|
||||
|
||||
@Composable
|
||||
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
|
||||
BrowseSourceCompactGridItem(
|
||||
manga = manga,
|
||||
|
@ -4,11 +4,9 @@ import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import eu.kanade.tachiyomi.R
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
|
||||
@Composable
|
||||
fun RemoveMangaDialog(
|
||||
@ -20,7 +18,7 @@ fun RemoveMangaDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismissRequest) {
|
||||
Text(text = stringResource(R.string.action_cancel))
|
||||
Text(text = stringResource(MR.strings.action_cancel))
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
@ -30,14 +28,14 @@ fun RemoveMangaDialog(
|
||||
onConfirm()
|
||||
},
|
||||
) {
|
||||
Text(text = stringResource(R.string.action_remove))
|
||||
Text(text = stringResource(MR.strings.action_remove))
|
||||
}
|
||||
},
|
||||
title = {
|
||||
Text(text = stringResource(R.string.are_you_sure))
|
||||
Text(text = stringResource(MR.strings.are_you_sure))
|
||||
},
|
||||
text = {
|
||||
Text(text = stringResource(R.string.remove_manga, mangaToRemove.title))
|
||||
Text(text = stringResource(MR.strings.remove_manga, mangaToRemove.title))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
@ -1,21 +1,19 @@
|
||||
package eu.kanade.presentation.browse.components
|
||||
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.paging.LoadState
|
||||
import androidx.paging.compose.LazyPagingItems
|
||||
import androidx.paging.compose.items
|
||||
import eu.kanade.presentation.browse.InLibraryBadge
|
||||
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 eu.kanade.presentation.library.components.CommonMangaItemDefaults
|
||||
import eu.kanade.presentation.library.components.MangaListItem
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.domain.manga.model.MangaCover
|
||||
import tachiyomi.presentation.core.util.plus
|
||||
|
||||
@Composable
|
||||
fun BrowseSourceList(
|
||||
@ -33,9 +31,8 @@ fun BrowseSourceList(
|
||||
}
|
||||
}
|
||||
|
||||
items(mangaList) { mangaflow ->
|
||||
mangaflow ?: return@items
|
||||
val manga by mangaflow.collectAsState()
|
||||
items(count = mangaList.itemCount) { index ->
|
||||
val manga by mangaList[index]?.collectAsState() ?: return@items
|
||||
BrowseSourceListItem(
|
||||
manga = manga,
|
||||
onClick = { onMangaClick(manga) },
|
||||
@ -52,7 +49,7 @@ fun BrowseSourceList(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BrowseSourceListItem(
|
||||
private fun BrowseSourceListItem(
|
||||
manga: Manga,
|
||||
onClick: () -> Unit = {},
|
||||
onLongClick: () -> Unit = onClick,
|
||||
|
@ -10,7 +10,7 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun BrowseSourceLoadingItem() {
|
||||
internal fun BrowseSourceLoadingItem() {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
|
@ -1,10 +1,8 @@
|
||||
package eu.kanade.presentation.browse.components
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ViewList
|
||||
import androidx.compose.material.icons.automirrored.filled.ViewList
|
||||
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.TopAppBarScrollBehavior
|
||||
import androidx.compose.runtime.Composable
|
||||
@ -12,34 +10,40 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import eu.kanade.presentation.components.AppBar
|
||||
import eu.kanade.presentation.components.AppBarActions
|
||||
import eu.kanade.presentation.components.AppBarTitle
|
||||
import eu.kanade.presentation.components.DropdownMenu
|
||||
import eu.kanade.presentation.components.RadioMenuItem
|
||||
import eu.kanade.presentation.components.SearchToolbar
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.LocalSource
|
||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import tachiyomi.domain.library.model.LibraryDisplayMode
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import tachiyomi.source.local.LocalSource
|
||||
|
||||
@Composable
|
||||
fun BrowseSourceToolbar(
|
||||
searchQuery: String?,
|
||||
onSearchQueryChange: (String?) -> Unit,
|
||||
source: CatalogueSource?,
|
||||
source: Source?,
|
||||
displayMode: LibraryDisplayMode,
|
||||
onDisplayModeChange: (LibraryDisplayMode) -> Unit,
|
||||
navigateUp: () -> Unit,
|
||||
onWebViewClick: () -> Unit,
|
||||
onHelpClick: () -> Unit,
|
||||
onSettingsClick: () -> Unit,
|
||||
onSearch: (String) -> Unit,
|
||||
scrollBehavior: TopAppBarScrollBehavior? = null,
|
||||
) {
|
||||
// Avoid capturing unstable source in actions lambda
|
||||
val title = source?.name
|
||||
val isLocalSource = source is LocalSource
|
||||
val isConfigurableSource = source is ConfigurableSource
|
||||
|
||||
var selectingDisplayMode by remember { mutableStateOf(false) }
|
||||
|
||||
SearchToolbar(
|
||||
navigateUp = navigateUp,
|
||||
@ -49,49 +53,67 @@ fun BrowseSourceToolbar(
|
||||
onSearch = onSearch,
|
||||
onClickCloseSearch = navigateUp,
|
||||
actions = {
|
||||
var selectingDisplayMode by remember { mutableStateOf(false) }
|
||||
AppBarActions(
|
||||
actions = listOf(
|
||||
actions = persistentListOf<AppBar.AppBarAction>().builder()
|
||||
.apply {
|
||||
add(
|
||||
AppBar.Action(
|
||||
title = stringResource(R.string.action_display_mode),
|
||||
icon = if (displayMode == LibraryDisplayMode.List) Icons.Filled.ViewList else Icons.Filled.ViewModule,
|
||||
title = stringResource(MR.strings.action_display_mode),
|
||||
icon = if (displayMode == LibraryDisplayMode.List) {
|
||||
Icons.AutoMirrored.Filled.ViewList
|
||||
} else {
|
||||
Icons.Filled.ViewModule
|
||||
},
|
||||
onClick = { selectingDisplayMode = true },
|
||||
),
|
||||
)
|
||||
if (isLocalSource) {
|
||||
AppBar.Action(
|
||||
title = stringResource(R.string.label_help),
|
||||
icon = Icons.Outlined.Help,
|
||||
add(
|
||||
AppBar.OverflowAction(
|
||||
title = stringResource(MR.strings.label_help),
|
||||
onClick = onHelpClick,
|
||||
)
|
||||
} else {
|
||||
AppBar.Action(
|
||||
title = stringResource(R.string.action_web_view),
|
||||
icon = Icons.Outlined.Public,
|
||||
onClick = onWebViewClick,
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
} else {
|
||||
add(
|
||||
AppBar.OverflowAction(
|
||||
title = stringResource(MR.strings.action_open_in_web_view),
|
||||
onClick = onWebViewClick,
|
||||
),
|
||||
)
|
||||
}
|
||||
if (isConfigurableSource) {
|
||||
add(
|
||||
AppBar.OverflowAction(
|
||||
title = stringResource(MR.strings.action_settings),
|
||||
onClick = onSettingsClick,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
.build(),
|
||||
)
|
||||
|
||||
DropdownMenu(
|
||||
expanded = selectingDisplayMode,
|
||||
onDismissRequest = { selectingDisplayMode = false },
|
||||
) {
|
||||
RadioMenuItem(
|
||||
text = { Text(text = stringResource(R.string.action_display_comfortable_grid)) },
|
||||
text = { Text(text = stringResource(MR.strings.action_display_comfortable_grid)) },
|
||||
isChecked = displayMode == LibraryDisplayMode.ComfortableGrid,
|
||||
) {
|
||||
selectingDisplayMode = false
|
||||
onDisplayModeChange(LibraryDisplayMode.ComfortableGrid)
|
||||
}
|
||||
RadioMenuItem(
|
||||
text = { Text(text = stringResource(R.string.action_display_grid)) },
|
||||
text = { Text(text = stringResource(MR.strings.action_display_grid)) },
|
||||
isChecked = displayMode == LibraryDisplayMode.CompactGrid,
|
||||
) {
|
||||
selectingDisplayMode = false
|
||||
onDisplayModeChange(LibraryDisplayMode.CompactGrid)
|
||||
}
|
||||
RadioMenuItem(
|
||||
text = { Text(text = stringResource(R.string.action_display_list)) },
|
||||
text = { Text(text = stringResource(MR.strings.action_display_list)) },
|
||||
isChecked = displayMode == LibraryDisplayMode.List,
|
||||
) {
|
||||
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
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
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.items
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.getValue
|
||||
import eu.kanade.presentation.util.padding
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.kanade.presentation.library.components.CommonMangaItemDefaults
|
||||
import eu.kanade.presentation.library.components.MangaComfortableGridItem
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.domain.manga.model.MangaCover
|
||||
import tachiyomi.domain.manga.model.asMangaCover
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
|
||||
@Composable
|
||||
fun GlobalSearchCardRow(
|
||||
@ -19,13 +30,18 @@ fun GlobalSearchCardRow(
|
||||
onClick: (Manga) -> Unit,
|
||||
onLongClick: (Manga) -> Unit,
|
||||
) {
|
||||
if (titles.isEmpty()) {
|
||||
EmptyResultItem()
|
||||
return
|
||||
}
|
||||
|
||||
LazyRow(
|
||||
contentPadding = PaddingValues(MaterialTheme.padding.small),
|
||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.tiny),
|
||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
|
||||
) {
|
||||
items(titles) {
|
||||
val title by getManga(it)
|
||||
GlobalSearchCard(
|
||||
MangaItem(
|
||||
title = title.title,
|
||||
cover = title.asMangaCover(),
|
||||
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(MR.strings.no_results_found),
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
horizontal = MaterialTheme.padding.medium,
|
||||
vertical = MaterialTheme.padding.small,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.ArrowForward
|
||||
import androidx.compose.material.icons.automirrored.outlined.ArrowForward
|
||||
import androidx.compose.material.icons.outlined.Error
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Icon
|
||||
@ -21,11 +21,11 @@ import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.kanade.presentation.util.padding
|
||||
import eu.kanade.tachiyomi.R
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
|
||||
@Composable
|
||||
fun GlobalSearchResultItem(
|
||||
@ -39,7 +39,7 @@ fun GlobalSearchResultItem(
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
start = MaterialTheme.padding.medium,
|
||||
end = MaterialTheme.padding.tiny,
|
||||
end = MaterialTheme.padding.extraSmall,
|
||||
)
|
||||
.fillMaxWidth()
|
||||
.clickable(onClick = onClick),
|
||||
@ -54,25 +54,13 @@ fun GlobalSearchResultItem(
|
||||
Text(text = subtitle)
|
||||
}
|
||||
IconButton(onClick = onClick) {
|
||||
Icon(imageVector = Icons.Outlined.ArrowForward, contentDescription = null)
|
||||
Icon(imageVector = Icons.AutoMirrored.Outlined.ArrowForward, contentDescription = null)
|
||||
}
|
||||
}
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun GlobalSearchEmptyResultItem() {
|
||||
Text(
|
||||
text = stringResource(R.string.no_results_found),
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
horizontal = MaterialTheme.padding.medium,
|
||||
vertical = MaterialTheme.padding.small,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun GlobalSearchLoadingResultItem() {
|
||||
Box(
|
||||
@ -104,7 +92,7 @@ fun GlobalSearchErrorResultItem(message: String?) {
|
||||
Icon(imageVector = Icons.Outlined.Error, contentDescription = null)
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Text(
|
||||
text = message ?: stringResource(R.string.unknown_error),
|
||||
text = message ?: stringResource(MR.strings.unknown_error),
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
|
@ -1,13 +1,36 @@
|
||||
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.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
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.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||
import androidx.compose.material3.VerticalDivider
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import eu.kanade.presentation.components.SearchToolbar
|
||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SourceFilter
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
|
||||
@Composable
|
||||
fun GlobalSearchToolbar(
|
||||
@ -17,8 +40,13 @@ fun GlobalSearchToolbar(
|
||||
navigateUp: () -> Unit,
|
||||
onChangeSearchQuery: (String?) -> Unit,
|
||||
onSearch: (String) -> Unit,
|
||||
sourceFilter: SourceFilter,
|
||||
onChangeSearchFilter: (SourceFilter) -> Unit,
|
||||
onlyShowHasResults: Boolean,
|
||||
onToggleResults: () -> Unit,
|
||||
scrollBehavior: TopAppBarScrollBehavior,
|
||||
) {
|
||||
Column(modifier = Modifier.background(MaterialTheme.colorScheme.surface)) {
|
||||
Box {
|
||||
SearchToolbar(
|
||||
searchQuery = searchQuery,
|
||||
@ -28,13 +56,73 @@ fun GlobalSearchToolbar(
|
||||
navigateUp = navigateUp,
|
||||
scrollBehavior = scrollBehavior,
|
||||
)
|
||||
if (progress in 1 until total) {
|
||||
if (progress in 1..<total) {
|
||||
LinearProgressIndicator(
|
||||
progress = progress / total.toFloat(),
|
||||
progress = { progress / total.toFloat() },
|
||||
modifier = Modifier
|
||||
.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(MR.strings.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(MR.strings.all))
|
||||
},
|
||||
)
|
||||
|
||||
VerticalDivider()
|
||||
|
||||
FilterChip(
|
||||
selected = onlyShowHasResults,
|
||||
onClick = { onToggleResults() },
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.FilterList,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.size(FilterChipDefaults.IconSize),
|
||||
)
|
||||
},
|
||||
label = {
|
||||
Text(text = stringResource(MR.strings.has_results))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
HorizontalDivider()
|
||||
}
|
||||
}
|
||||
|
@ -2,19 +2,20 @@ package eu.kanade.presentation.category
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import eu.kanade.tachiyomi.R
|
||||
import tachiyomi.core.i18n.stringResource
|
||||
import tachiyomi.domain.category.model.Category
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
|
||||
val Category.visualName: String
|
||||
@Composable
|
||||
get() = when {
|
||||
isSystemCategory -> stringResource(R.string.label_default)
|
||||
isSystemCategory -> stringResource(MR.strings.label_default)
|
||||
else -> name
|
||||
}
|
||||
|
||||
fun Category.visualName(context: Context): String =
|
||||
when {
|
||||
isSystemCategory -> context.getString(R.string.label_default)
|
||||
isSystemCategory -> context.stringResource(MR.strings.label_default)
|
||||
else -> name
|
||||
}
|
||||
|
@ -1,28 +1,37 @@
|
||||
package eu.kanade.presentation.category
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
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.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.SortByAlpha
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
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.CategoryListItem
|
||||
import eu.kanade.presentation.components.AppBar
|
||||
import eu.kanade.presentation.components.EmptyScreen
|
||||
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.presentation.components.AppBarActions
|
||||
import eu.kanade.tachiyomi.ui.category.CategoryScreenState
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import tachiyomi.domain.category.model.Category
|
||||
import tachiyomi.i18n.MR
|
||||
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.i18n.stringResource
|
||||
import tachiyomi.presentation.core.screens.EmptyScreen
|
||||
import tachiyomi.presentation.core.util.plus
|
||||
|
||||
@Composable
|
||||
fun CategoryScreen(
|
||||
state: CategoryScreenState.Success,
|
||||
onClickCreate: () -> Unit,
|
||||
onClickSortAlphabetically: () -> Unit,
|
||||
onClickRename: (Category) -> Unit,
|
||||
onClickDelete: (Category) -> Unit,
|
||||
onClickMoveUp: (Category) -> Unit,
|
||||
@ -33,8 +42,19 @@ fun CategoryScreen(
|
||||
Scaffold(
|
||||
topBar = { scrollBehavior ->
|
||||
AppBar(
|
||||
title = stringResource(R.string.action_edit_categories),
|
||||
title = stringResource(MR.strings.action_edit_categories),
|
||||
navigateUp = navigateUp,
|
||||
actions = {
|
||||
AppBarActions(
|
||||
persistentListOf(
|
||||
AppBar.Action(
|
||||
title = stringResource(MR.strings.action_sort),
|
||||
icon = Icons.Outlined.SortByAlpha,
|
||||
onClick = onClickSortAlphabetically,
|
||||
),
|
||||
),
|
||||
)
|
||||
},
|
||||
scrollBehavior = scrollBehavior,
|
||||
)
|
||||
},
|
||||
@ -47,7 +67,7 @@ fun CategoryScreen(
|
||||
) { paddingValues ->
|
||||
if (state.isEmpty) {
|
||||
EmptyScreen(
|
||||
textResource = R.string.information_empty_category,
|
||||
stringRes = MR.strings.information_empty_category,
|
||||
modifier = Modifier.padding(paddingValues),
|
||||
)
|
||||
return@Scaffold
|
||||
@ -56,7 +76,9 @@ fun CategoryScreen(
|
||||
CategoryContent(
|
||||
categories = state.categories,
|
||||
lazyListState = lazyListState,
|
||||
paddingValues = paddingValues + topSmallPaddingValues + PaddingValues(horizontal = MaterialTheme.padding.medium),
|
||||
paddingValues = paddingValues +
|
||||
topSmallPaddingValues +
|
||||
PaddingValues(horizontal = MaterialTheme.padding.medium),
|
||||
onClickRename = onClickRename,
|
||||
onClickDelete = onClickDelete,
|
||||
onMoveUp = onClickMoveUp,
|
||||
@ -64,3 +86,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,34 +1,52 @@
|
||||
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.Checkbox
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TriStateCheckbox
|
||||
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.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.core.preference.asToggleableState
|
||||
import eu.kanade.presentation.category.visualName
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.delay
|
||||
import tachiyomi.core.preference.CheckboxState
|
||||
import tachiyomi.domain.category.model.Category
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@Composable
|
||||
fun CategoryCreateDialog(
|
||||
onDismissRequest: () -> Unit,
|
||||
onCreate: (String) -> Unit,
|
||||
categories: List<Category>,
|
||||
categories: ImmutableList<String>,
|
||||
) {
|
||||
var name by remember { mutableStateOf("") }
|
||||
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
val nameAlreadyExists = remember(name) { categories.anyWithName(name) }
|
||||
val nameAlreadyExists = remember(name) { categories.contains(name) }
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
@ -40,25 +58,32 @@ fun CategoryCreateDialog(
|
||||
onDismissRequest()
|
||||
},
|
||||
) {
|
||||
Text(text = stringResource(R.string.action_add))
|
||||
Text(text = stringResource(MR.strings.action_add))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismissRequest) {
|
||||
Text(text = stringResource(R.string.action_cancel))
|
||||
Text(text = stringResource(MR.strings.action_cancel))
|
||||
}
|
||||
},
|
||||
title = {
|
||||
Text(text = stringResource(R.string.action_add_category))
|
||||
Text(text = stringResource(MR.strings.action_add_category))
|
||||
},
|
||||
text = {
|
||||
OutlinedTextField(
|
||||
modifier = Modifier.focusRequester(focusRequester),
|
||||
modifier = Modifier
|
||||
.focusRequester(focusRequester),
|
||||
value = name,
|
||||
onValueChange = { name = it },
|
||||
label = { Text(text = stringResource(R.string.name)) },
|
||||
label = {
|
||||
Text(text = stringResource(MR.strings.name))
|
||||
},
|
||||
supportingText = {
|
||||
val msgRes = if (name.isNotEmpty() && nameAlreadyExists) R.string.error_category_exists else R.string.information_required_plain
|
||||
val msgRes = if (name.isNotEmpty() && nameAlreadyExists) {
|
||||
MR.strings.error_category_exists
|
||||
} else {
|
||||
MR.strings.information_required_plain
|
||||
}
|
||||
Text(text = stringResource(msgRes))
|
||||
},
|
||||
isError = name.isNotEmpty() && nameAlreadyExists,
|
||||
@ -78,14 +103,14 @@ fun CategoryCreateDialog(
|
||||
fun CategoryRenameDialog(
|
||||
onDismissRequest: () -> Unit,
|
||||
onRename: (String) -> Unit,
|
||||
categories: List<Category>,
|
||||
category: Category,
|
||||
categories: ImmutableList<String>,
|
||||
category: String,
|
||||
) {
|
||||
var name by remember { mutableStateOf(category.name) }
|
||||
var name by remember { mutableStateOf(category) }
|
||||
var valueHasChanged by remember { mutableStateOf(false) }
|
||||
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
val nameAlreadyExists = remember(name) { categories.anyWithName(name) }
|
||||
val nameAlreadyExists = remember(name) { categories.contains(name) }
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
@ -97,16 +122,16 @@ fun CategoryRenameDialog(
|
||||
onDismissRequest()
|
||||
},
|
||||
) {
|
||||
Text(text = stringResource(android.R.string.ok))
|
||||
Text(text = stringResource(MR.strings.action_ok))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismissRequest) {
|
||||
Text(text = stringResource(R.string.action_cancel))
|
||||
Text(text = stringResource(MR.strings.action_cancel))
|
||||
}
|
||||
},
|
||||
title = {
|
||||
Text(text = stringResource(R.string.action_rename_category))
|
||||
Text(text = stringResource(MR.strings.action_rename_category))
|
||||
},
|
||||
text = {
|
||||
OutlinedTextField(
|
||||
@ -116,9 +141,13 @@ fun CategoryRenameDialog(
|
||||
valueHasChanged = name != it
|
||||
name = it
|
||||
},
|
||||
label = { Text(text = stringResource(R.string.name)) },
|
||||
label = { Text(text = stringResource(MR.strings.name)) },
|
||||
supportingText = {
|
||||
val msgRes = if (valueHasChanged && nameAlreadyExists) R.string.error_category_exists else R.string.information_required_plain
|
||||
val msgRes = if (valueHasChanged && nameAlreadyExists) {
|
||||
MR.strings.error_category_exists
|
||||
} else {
|
||||
MR.strings.information_required_plain
|
||||
}
|
||||
Text(text = stringResource(msgRes))
|
||||
},
|
||||
isError = valueHasChanged && nameAlreadyExists,
|
||||
@ -138,7 +167,7 @@ fun CategoryRenameDialog(
|
||||
fun CategoryDeleteDialog(
|
||||
onDismissRequest: () -> Unit,
|
||||
onDelete: () -> Unit,
|
||||
category: Category,
|
||||
category: String,
|
||||
) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
@ -146,24 +175,158 @@ fun CategoryDeleteDialog(
|
||||
TextButton(onClick = {
|
||||
onDelete()
|
||||
onDismissRequest()
|
||||
},) {
|
||||
Text(text = stringResource(android.R.string.ok))
|
||||
}) {
|
||||
Text(text = stringResource(MR.strings.action_ok))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismissRequest) {
|
||||
Text(text = stringResource(R.string.action_cancel))
|
||||
Text(text = stringResource(MR.strings.action_cancel))
|
||||
}
|
||||
},
|
||||
title = {
|
||||
Text(text = stringResource(R.string.delete_category))
|
||||
Text(text = stringResource(MR.strings.delete_category))
|
||||
},
|
||||
text = {
|
||||
Text(text = stringResource(R.string.delete_category_confirmation, category.name))
|
||||
Text(text = stringResource(MR.strings.delete_category_confirmation, category))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
internal fun List<Category>.anyWithName(name: String): Boolean {
|
||||
return any { name == it.name }
|
||||
@Composable
|
||||
fun CategorySortAlphabeticallyDialog(
|
||||
onDismissRequest: () -> Unit,
|
||||
onSort: () -> Unit,
|
||||
) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
onSort()
|
||||
onDismissRequest()
|
||||
}) {
|
||||
Text(text = stringResource(MR.strings.action_ok))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismissRequest) {
|
||||
Text(text = stringResource(MR.strings.action_cancel))
|
||||
}
|
||||
},
|
||||
title = {
|
||||
Text(text = stringResource(MR.strings.action_sort_category))
|
||||
},
|
||||
text = {
|
||||
Text(text = stringResource(MR.strings.sort_category_confirmation))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ChangeCategoryDialog(
|
||||
initialSelection: ImmutableList<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(MR.strings.action_edit_categories))
|
||||
}
|
||||
},
|
||||
title = {
|
||||
Text(text = stringResource(MR.strings.action_move_category))
|
||||
},
|
||||
text = {
|
||||
Text(text = stringResource(MR.strings.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(MR.strings.action_edit))
|
||||
}
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
tachiyomi.presentation.core.components.material.TextButton(onClick = onDismissRequest) {
|
||||
Text(text = stringResource(MR.strings.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(MR.strings.action_ok))
|
||||
}
|
||||
}
|
||||
},
|
||||
title = {
|
||||
Text(text = stringResource(MR.strings.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().toImmutableList()
|
||||
}
|
||||
}
|
||||
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),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
@ -6,21 +6,24 @@ import androidx.compose.material.icons.outlined.Add
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
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 androidx.compose.ui.Modifier
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.material.ExtendedFloatingActionButton
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import tachiyomi.presentation.core.util.isScrolledToEnd
|
||||
import tachiyomi.presentation.core.util.isScrollingUp
|
||||
|
||||
@Composable
|
||||
fun CategoryFloatingActionButton(
|
||||
lazyListState: LazyListState,
|
||||
onCreate: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
ExtendedFloatingActionButton(
|
||||
text = { Text(text = stringResource(R.string.action_add)) },
|
||||
icon = { Icon(imageVector = Icons.Outlined.Add, contentDescription = "") },
|
||||
text = { Text(text = stringResource(MR.strings.action_add)) },
|
||||
icon = { Icon(imageVector = Icons.Outlined.Add, contentDescription = null) },
|
||||
onClick = onCreate,
|
||||
expanded = lazyListState.isScrollingUp() || lazyListState.isScrolledToEnd(),
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
@ -6,11 +6,11 @@ import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.outlined.Label
|
||||
import androidx.compose.material.icons.outlined.ArrowDropDown
|
||||
import androidx.compose.material.icons.outlined.ArrowDropUp
|
||||
import androidx.compose.material.icons.outlined.Delete
|
||||
import androidx.compose.material.icons.outlined.Edit
|
||||
import androidx.compose.material.icons.outlined.Label
|
||||
import androidx.compose.material3.ElevatedCard
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
@ -19,14 +19,13 @@ import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import eu.kanade.presentation.util.padding
|
||||
import eu.kanade.tachiyomi.R
|
||||
import tachiyomi.domain.category.model.Category
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
|
||||
@Composable
|
||||
fun CategoryListItem(
|
||||
modifier: Modifier = Modifier,
|
||||
category: Category,
|
||||
canMoveUp: Boolean,
|
||||
canMoveDown: Boolean,
|
||||
@ -34,6 +33,7 @@ fun CategoryListItem(
|
||||
onMoveDown: (Category) -> Unit,
|
||||
onRename: () -> Unit,
|
||||
onDelete: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
ElevatedCard(
|
||||
modifier = modifier,
|
||||
@ -49,7 +49,7 @@ fun CategoryListItem(
|
||||
),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(imageVector = Icons.Outlined.Label, contentDescription = "")
|
||||
Icon(imageVector = Icons.AutoMirrored.Outlined.Label, contentDescription = null)
|
||||
Text(
|
||||
text = category.name,
|
||||
modifier = Modifier
|
||||
@ -61,20 +61,23 @@ fun CategoryListItem(
|
||||
onClick = { onMoveUp(category) },
|
||||
enabled = canMoveUp,
|
||||
) {
|
||||
Icon(imageVector = Icons.Outlined.ArrowDropUp, contentDescription = "")
|
||||
Icon(imageVector = Icons.Outlined.ArrowDropUp, contentDescription = null)
|
||||
}
|
||||
IconButton(
|
||||
onClick = { onMoveDown(category) },
|
||||
enabled = canMoveDown,
|
||||
) {
|
||||
Icon(imageVector = Icons.Outlined.ArrowDropDown, contentDescription = "")
|
||||
Icon(imageVector = Icons.Outlined.ArrowDropDown, contentDescription = null)
|
||||
}
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
IconButton(onClick = onRename) {
|
||||
Icon(imageVector = Icons.Outlined.Edit, contentDescription = stringResource(R.string.action_rename_category))
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Edit,
|
||||
contentDescription = stringResource(MR.strings.action_rename_category),
|
||||
)
|
||||
}
|
||||
IconButton(onClick = onDelete) {
|
||||
Icon(imageVector = Icons.Outlined.Delete, contentDescription = stringResource(R.string.action_delete))
|
||||
Icon(imageVector = Icons.Outlined.Delete, contentDescription = stringResource(MR.strings.action_delete))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,75 +1,25 @@
|
||||
package eu.kanade.presentation.components
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.with
|
||||
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.animation.togetherWith
|
||||
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.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.IntOffset
|
||||
import androidx.compose.ui.unit.Velocity
|
||||
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.screen.Screen
|
||||
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 kotlinx.coroutines.delay
|
||||
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)
|
||||
import tachiyomi.presentation.core.components.AdaptiveSheet as AdaptiveSheetImpl
|
||||
|
||||
@OptIn(InternalVoyagerApi::class)
|
||||
@Composable
|
||||
fun NavigatorAdaptiveSheet(
|
||||
screen: Screen,
|
||||
@ -88,7 +38,7 @@ fun NavigatorAdaptiveSheet(
|
||||
ScreenTransition(
|
||||
navigator = sheetNavigator,
|
||||
transition = {
|
||||
fadeIn(animationSpec = tween(220, delayMillis = 90)) with
|
||||
fadeIn(animationSpec = tween(220, delayMillis = 90)) togetherWith
|
||||
fadeOut(animationSpec = tween(90))
|
||||
},
|
||||
)
|
||||
@ -121,224 +71,31 @@ fun NavigatorAdaptiveSheet(
|
||||
*/
|
||||
@Composable
|
||||
fun AdaptiveSheet(
|
||||
onDismissRequest: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
tonalElevation: Dp = 1.dp,
|
||||
enableSwipeDismiss: Boolean = true,
|
||||
onDismissRequest: () -> Unit,
|
||||
content: @Composable (PaddingValues) -> Unit,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
val isTabletUi = isTabletUi()
|
||||
|
||||
Dialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
properties = dialogProperties,
|
||||
) {
|
||||
AdaptiveSheetImpl(
|
||||
modifier = modifier,
|
||||
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,
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
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) {
|
||||
targetAlpha = 1f
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val swipeState = rememberSwipeableState(
|
||||
initialValue = 1,
|
||||
animationSpec = SheetAnimationSpec,
|
||||
)
|
||||
val internalOnDismissRequest: () -> Unit = { if (swipeState.currentValue == 0) scope.launch { swipeState.animateTo(1) } }
|
||||
BoxWithConstraints(
|
||||
modifier = Modifier
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = null,
|
||||
onClick = internalOnDismissRequest,
|
||||
)
|
||||
.fillMaxSize(),
|
||||
contentAlignment = Alignment.BottomCenter,
|
||||
) {
|
||||
val fullHeight = constraints.maxHeight.toFloat()
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Yoinked from Swipeable.kt with modifications to disable
|
||||
*/
|
||||
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
|
||||
}
|
||||
private val dialogProperties = DialogProperties(
|
||||
usePlatformDefaultWidth = false,
|
||||
decorFitsSystemWindows = false,
|
||||
)
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user