mirror of
https://github.com/mihonapp/mihon.git
synced 2026-11-02 04:56:38 -08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9612d955db | ||
|
|
5ca2a39328 | ||
|
|
fb2b224030 | ||
|
|
ee454f3a82 | ||
|
|
4fd0081817 | ||
|
|
68dbd7a185 | ||
|
|
7475f4554c | ||
|
|
8e688ed51e | ||
|
|
cca6b5e2e7 | ||
|
|
4c316ccc3a | ||
|
|
7c7392a533 | ||
|
|
d4fe03cd18 | ||
|
|
e9670d8a12 | ||
|
|
87e808548b | ||
|
|
51b61a36e8 | ||
|
|
010ac71a84 | ||
|
|
9dbb7148c5 | ||
|
|
a999e49f2f | ||
|
|
ae749bb4f8 | ||
|
|
649fc83ffa | ||
|
|
6dffca1593 | ||
|
|
07b9fb695c | ||
|
|
c279291e29 | ||
|
|
abb126ad20 | ||
|
|
8457fb91e0 | ||
|
|
143a060ac9 | ||
|
|
8912ef5b5c | ||
|
|
79fc3a0a8a | ||
|
|
c8b820a08f | ||
|
|
f45d0ad31d | ||
|
|
b5856135c5 | ||
|
|
039138ce99 | ||
|
|
35bd0a4b35 | ||
|
|
dd4e9f00f1 | ||
|
|
bce1f71958 | ||
|
|
d58130a5fc | ||
|
|
e562a9b4f8 | ||
|
|
ba44815254 | ||
|
|
fc20410ceb | ||
|
|
b454c1b469 | ||
|
|
f02bf5ba7e | ||
|
|
ba16158b13 | ||
|
|
8066b8dfc9 | ||
|
|
260e31cd2c | ||
|
|
b15af71b94 | ||
|
|
3e5d593e28 | ||
|
|
c0c0909999 | ||
|
|
c46317f099 | ||
|
|
f9c5fa9011 | ||
|
|
a82d27c3a6 | ||
|
|
611578313e | ||
|
|
f921ec07a6 | ||
|
|
e21414b229 | ||
|
|
e42e01049d | ||
|
|
6c1b0702ba | ||
|
|
292381662a | ||
|
|
44c1e1915d | ||
|
|
8972882a34 | ||
|
|
e9234f205d | ||
|
|
adc9ee74ec | ||
|
|
be44a81fb6 | ||
|
|
88d4071cce | ||
|
|
f28b2d887e | ||
|
|
287cbbb986 | ||
|
|
d3f10772f2 | ||
|
|
785f4fd1ed | ||
|
|
40f552981a | ||
|
|
8c0419657b | ||
|
|
36ec043a5c | ||
|
|
0c1be369b0 | ||
|
|
157679bba5 | ||
|
|
5b3505bf5f | ||
|
|
2f5b9c1641 | ||
|
|
24bd3c95a5 | ||
|
|
7f2b4962b7 | ||
|
|
8d39153569 | ||
|
|
e998904fb6 | ||
|
|
d1e71ab58f | ||
|
|
a80027ff36 | ||
|
|
dd46413e10 | ||
|
|
ab7ee00c48 | ||
|
|
fd4f73c908 | ||
|
|
a6ee4bc596 | ||
|
|
6d4ed6cb22 | ||
|
|
76b146f3d4 | ||
|
|
5910a51eb4 | ||
|
|
1e471a414c | ||
|
|
06d6128116 | ||
|
|
6a329386c2 | ||
|
|
179bfaca72 | ||
|
|
5df78e305e | ||
|
|
940272f75a | ||
|
|
7ff97761b9 | ||
|
|
9a6dea804d | ||
|
|
fb8e640d69 | ||
|
|
1f85a5a8db | ||
|
|
730e43fd14 | ||
|
|
61cd786e7f | ||
|
|
6490c41e63 | ||
|
|
a5d584eeb0 | ||
|
|
f027de8c2e | ||
|
|
7aacaa3490 | ||
|
|
f82a7a68f4 | ||
|
|
d1c5c12bdd | ||
|
|
f35bc341ae | ||
|
|
a7a8cc8343 | ||
|
|
4c88f02646 | ||
|
|
a4780e4edb | ||
|
|
db45dda525 | ||
|
|
9abc830769 | ||
|
|
9409740d7e | ||
|
|
38e93086c8 | ||
|
|
01d302febd | ||
|
|
6dfa10ffac | ||
|
|
f2464a1ba6 | ||
|
|
98ba693054 | ||
|
|
972c9d43da | ||
|
|
866d045c7a | ||
|
|
65d7110e1e | ||
|
|
2d1d2e4cab | ||
|
|
93cc075116 | ||
|
|
ec614bb4c3 | ||
|
|
59fdf7e7fa | ||
|
|
7451c228e6 | ||
|
|
9b5e0ae735 | ||
|
|
7f4c55176f | ||
|
|
9b4c3115db | ||
|
|
f835398094 | ||
|
|
3542143b40 | ||
|
|
ba4c04fe74 | ||
|
|
087e38ad47 | ||
|
|
09edf7dc8d | ||
|
|
4f33486aa4 | ||
|
|
2d86a70ce2 | ||
|
|
0938411053 | ||
|
|
ed17ece4e3 | ||
|
|
b74e010bfe | ||
|
|
4b48a84ec9 | ||
|
|
9a77baedcc | ||
|
|
2b4b8472bb | ||
|
|
553762fae8 | ||
|
|
8bfa495f34 | ||
|
|
fc5592ff75 | ||
|
|
0044cc73ec | ||
|
|
6ee529c5aa | ||
|
|
4df0e6b168 | ||
|
|
986f46c09a | ||
|
|
f8fff318ba | ||
|
|
306befb214 | ||
|
|
a4abb6292b | ||
|
|
c67a33f3d4 | ||
|
|
532575e29e | ||
|
|
f52d890e7f | ||
|
|
449c4e08e7 | ||
|
|
57666e9207 | ||
|
|
73fef961e9 | ||
|
|
93a163f96e | ||
|
|
61680580dd | ||
|
|
0de91a9c53 | ||
|
|
2c979d2d32 | ||
|
|
2b9cb4ca5a | ||
|
|
427e105840 | ||
|
|
12fbb13df5 | ||
|
|
c69b14c975 | ||
|
|
debab6c261 | ||
|
|
424bbc53b8 | ||
|
|
71cd94a31d | ||
|
|
01e3f8c605 | ||
|
|
504ec2afae | ||
|
|
385d076d43 | ||
|
|
5848ffb4c4 | ||
|
+13 |
51efde975b | ||
|
|
1e142c4255 | ||
|
|
a7179805ad | ||
|
|
1c43addeca | ||
|
|
b2cda03e3a | ||
|
|
fdc2d40a1d | ||
|
|
e6d8efe6f8 | ||
|
|
0e0bc6972a | ||
|
|
a5cd541bdf | ||
|
|
0fbaa1ecec | ||
|
+21 |
cbafe9bf3f | ||
|
|
7418f4e7bc | ||
|
|
2d1a8bbeff | ||
|
|
60e74b8d8f | ||
|
|
6ef6e81814 | ||
|
|
855918bbf4 | ||
|
|
6c6eff3ab8 | ||
|
|
d435dd9217 | ||
|
|
f7da5c856a | ||
|
|
737fdbfbf0 | ||
|
|
802958b1ea | ||
|
|
2292554d97 | ||
|
|
918cfb3dda | ||
|
|
5de262675a | ||
|
|
9f480564c2 | ||
|
|
d17ad1e002 | ||
|
|
1e054ea14d | ||
|
|
5f87f7e054 | ||
|
|
a5260487d3 | ||
|
|
0ce6f22cf5 | ||
|
|
35bb84705e | ||
|
|
e32231eeaa | ||
|
|
d0ee514044 | ||
|
|
44a9d96e90 | ||
|
|
3f845c12c8 | ||
|
|
6c0fa3a043 | ||
|
|
2810dd14c4 | ||
|
|
aebf11a749 | ||
|
|
3a64c8d65c | ||
|
|
f7cd820f2f | ||
|
|
5b69dbad81 | ||
|
|
a82baac3ef | ||
|
|
5e99306e5f | ||
|
|
3acd7eaca9 | ||
|
|
f3125ab897 | ||
|
|
21af65b100 | ||
|
|
056d9baeeb | ||
|
|
baad5c59bb | ||
|
|
34a33ed4f8 | ||
|
|
7f342ca076 | ||
|
|
b695940114 | ||
|
|
bb24e904a9 | ||
|
|
5fadea61e7 | ||
|
|
c1d1f6b161 | ||
|
|
ff3dedb6d8 | ||
|
|
7b41ebf8fe | ||
|
|
161617b95d | ||
|
|
1512ca63d7 | ||
|
|
6cfce8d425 | ||
|
|
2776e3edda | ||
|
|
7847cad0f4 | ||
|
|
deb7b33118 | ||
|
|
3f56685e0c | ||
|
|
0854bde12e | ||
|
|
ac249e3668 | ||
|
|
e3813028bd | ||
|
|
270b78ba95 | ||
|
|
46c8f07c12 | ||
|
|
a2c24ff042 | ||
|
|
9ec479266b | ||
|
|
6458246180 | ||
|
|
31d3c23717 | ||
|
|
497e266256 | ||
|
|
0974e787ad | ||
|
|
01bfaa99ad | ||
|
|
4b5a0539e5 | ||
|
|
598f634afb | ||
|
|
dee3daf145 | ||
|
|
9fe2c2e106 | ||
|
|
3cc0d2c324 | ||
|
|
f75f2598a3 | ||
|
|
c613b52503 | ||
|
|
8586d81e63 | ||
|
|
3e98c15b74 | ||
|
+1 |
2b532ea0c4 | ||
|
|
b2015d1ef3 | ||
|
|
1e5b1dc5e7 | ||
|
|
5fa5f7e159 | ||
|
|
00fb597ae9 | ||
|
|
7e78849ed0 | ||
|
|
4d5b0ea906 | ||
|
|
a81b7bce9a | ||
|
|
abdecb3cb4 | ||
|
|
ed30766a9f | ||
|
|
b1efee47f5 | ||
|
|
d00efa4bfc | ||
|
|
b1a9a05926 | ||
|
|
55675e7f80 | ||
|
|
b393088cfa | ||
|
|
77e88a2159 | ||
|
|
ecbec8f501 | ||
|
|
5c3ae37d52 | ||
|
|
581a89b7b0 | ||
|
|
6197768a01 | ||
|
|
d1f9a1fdaf | ||
|
|
db7f1663b7 | ||
|
|
a0a3adf864 | ||
|
|
437dd87225 | ||
|
|
cd1f426945 | ||
|
|
2506b04964 | ||
|
|
77a1a5afbd | ||
|
|
a494ebd791 | ||
|
|
c68efd0e86 | ||
|
|
ae37c1651a | ||
|
|
62288c08fb | ||
|
|
3cb9883d62 | ||
|
|
fab4cbd279 | ||
|
|
6c0058d230 | ||
|
|
2aec7d19c0 | ||
|
|
bb9767c9df | ||
|
|
864281af7d | ||
|
|
f6aad7497e | ||
|
|
0834884335 | ||
|
|
ed274e26fd | ||
|
|
f9156aa17a | ||
|
|
b6aa40a849 | ||
|
|
68aa4b003f | ||
|
|
aa25b3a14c | ||
|
|
869fa1a4b2 | ||
|
|
df6507256a | ||
|
+3 |
b765531b82 | ||
|
|
21c6357967 | ||
|
|
3d20bcc6b6 | ||
|
|
71deefb6fd | ||
|
|
8fceb737a4 | ||
|
|
81e0754c0d | ||
|
|
e287f08789 | ||
|
|
a73271aefa | ||
|
|
f7a1ecd251 | ||
|
|
f80c91081f | ||
|
|
55be95dd5d | ||
|
|
d7f3ceef5c | ||
|
|
1d8a2b05dd | ||
|
|
6d69903a56 | ||
|
|
0648e2eaaf | ||
|
|
7d4e1476ba | ||
|
+9 |
46a2471abb | ||
|
|
f63dc799f0 | ||
|
|
c114028a33 | ||
|
|
4296d056b5 | ||
|
|
7aaa891aef | ||
|
|
2ccb0ca163 | ||
|
|
44cd9a55d1 | ||
|
|
2a92f0b46a | ||
|
|
f37e81991e | ||
|
|
d15a05602a | ||
|
|
fbf5ae82f7 | ||
|
|
fdb223d657 | ||
|
|
54ae69eaa2 | ||
|
|
45b1e781eb | ||
|
|
069e5c91ac | ||
|
|
4ac9f3474b | ||
|
|
9c3cc1ca5f | ||
|
|
47d7c0f2ea | ||
|
|
0b5e9bb815 | ||
|
|
d72ae365cb | ||
|
|
e7ec672c8b | ||
|
|
628cb58862 | ||
|
|
e454cf8e9d | ||
|
|
baa0fb7ecb | ||
|
|
2d2cf0879f | ||
|
|
6981185f4b | ||
|
|
47aa9190d4 | ||
|
|
98705910e7 | ||
|
|
5ce7d00eb8 | ||
|
|
03229a3807 | ||
|
|
493140c7ff | ||
|
|
98bb731b4d | ||
|
|
c3b99aea07 | ||
|
|
ef1c529672 | ||
|
|
633eea4ced | ||
|
|
31a4d6ba39 | ||
|
|
e9b7c760a1 | ||
|
|
ef1e7ba907 | ||
|
|
526d79a975 | ||
|
|
b9ac9a0422 | ||
|
|
d171768cff | ||
|
|
cb2617dee0 | ||
|
|
b4635c41a8 | ||
|
|
217b20123c | ||
|
|
b2ca6bc5f3 | ||
|
|
a8d3e39779 | ||
|
|
3d3bd49d1c | ||
|
|
4a66b8b5df | ||
|
|
52f827c6ff | ||
|
|
aa434285b2 | ||
|
|
91e967128d | ||
|
|
abef1cb8c5 | ||
|
|
1d8e5d7eb1 | ||
|
|
75f506836c | ||
|
|
44780e74b5 | ||
|
|
8407439b8a | ||
|
|
ab58a9952e | ||
|
|
0ff654205f | ||
|
|
1d8789d800 | ||
|
|
a313c747f3 | ||
|
|
66b390c62a | ||
|
|
26abad0be0 | ||
|
|
68f293be45 | ||
|
|
41f4579c74 | ||
|
|
0787678c12 | ||
|
|
d0c79399b9 | ||
|
|
17aed02e56 | ||
|
|
88a9d44bb7 | ||
|
|
aa46039e6c | ||
|
|
3b078331a0 | ||
|
|
b8e5f22c01 | ||
|
|
c3bf7a78c0 | ||
|
|
94b3b5eaa2 | ||
|
|
27284a40ab | ||
|
|
76cc04c8f4 | ||
|
|
77580cd55f | ||
|
|
db095c87aa | ||
|
|
f3c6789dfb | ||
|
|
0772f72021 | ||
|
|
1623c188df | ||
|
|
dfed97edc8 | ||
|
|
f6632940fa | ||
|
|
ca2e8cf4de | ||
|
|
000e2d091b | ||
|
|
40f782e10c | ||
|
|
f306e65b25 | ||
|
|
d0f373df22 | ||
|
|
3fd76e4f93 | ||
|
|
d8c3440d37 | ||
|
|
a82ccea6f5 | ||
|
|
19f1d00fcb | ||
|
|
8c099aca90 | ||
|
|
6c6a07c0c0 | ||
|
|
8e323e30de | ||
|
|
91ccc0ca40 | ||
|
|
735cea35f9 | ||
|
|
41265cce32 | ||
|
|
176a5409b2 | ||
|
|
f23170bfaa | ||
|
|
bc7f7e70a1 | ||
|
|
a1b5ea811a | ||
|
+21 |
9d8d70dc61 | ||
|
|
06497622f6 | ||
|
|
c397d657d1 | ||
|
|
6f99c7b970 | ||
|
|
1cb38a1c33 | ||
|
|
b3e190c627 | ||
|
|
3bb734dde7 | ||
|
|
5f5a034112 | ||
|
|
eb5f57a48c | ||
|
|
903add50c9 | ||
|
|
c7e782c179 | ||
|
|
53724fd561 | ||
|
|
5f80403c5d | ||
|
|
d4ad76d576 | ||
|
|
46b5cce007 | ||
|
|
80541831bb | ||
|
|
ee6a9876ec | ||
|
|
df37d81dfb | ||
|
|
e99a50f612 | ||
|
|
50d0e8ae0a | ||
|
|
6552ffe315 | ||
|
|
381eb07ec9 | ||
|
|
3eafa16c64 | ||
|
|
672ff416c5 | ||
|
|
16376f509c | ||
|
|
56fde24532 | ||
|
|
518d2ab7df | ||
|
|
082bd66673 | ||
|
|
5880f4b0bd | ||
|
|
d156496cfa | ||
|
|
eb80ab410e | ||
|
|
5896d2c939 | ||
|
|
e3610cbe2c | ||
|
|
94cab02c1e | ||
|
|
f00cac265f | ||
|
|
aa6742bf3e | ||
|
|
4c37f4c764 | ||
|
|
430b13bb81 | ||
|
|
c15f7bee85 | ||
|
|
1a030420b1 | ||
|
|
a6f0cec083 | ||
|
|
6ae79d255d | ||
|
|
4b9974e20a | ||
|
|
509eee5dfb | ||
|
|
63a71fa447 | ||
|
|
d4327c8d48 | ||
|
|
5f1d76a73a | ||
|
|
35f6a607b8 | ||
|
|
378518c0fd | ||
|
|
69fb33887f | ||
|
|
f87c4d55e3 | ||
|
|
c612a6ca8d | ||
|
|
a1ad33baef | ||
|
|
8c27bc07fc | ||
|
|
95bc599ac0 | ||
|
|
cd27b10750 | ||
|
|
7f8851de6a | ||
|
|
a330258f6a | ||
|
|
d28c605a89 | ||
|
|
87c1675882 | ||
|
|
b446d2a6b1 | ||
|
|
748211c19b | ||
|
|
1fd02d0c82 | ||
|
|
4951f0bddd | ||
|
|
d391566832 | ||
|
|
0b564f1f20 | ||
|
|
b639e44a03 | ||
|
|
f68c1bcc9e | ||
|
|
4fabe9b405 | ||
|
|
ae3aa7926a | ||
|
|
82338d7d64 | ||
|
|
3b94df6108 | ||
|
|
5bb92a75c7 | ||
|
|
48179083f1 | ||
|
|
b4a3ec0b32 | ||
|
|
cb2a0ea570 | ||
|
|
9c3519046f | ||
|
|
da9b5d9b9d | ||
|
|
09d3e16183 | ||
|
|
cfefa51296 | ||
|
|
b65825b3ca | ||
|
|
981b16094a | ||
|
|
bb877e4b26 | ||
|
|
5b8adfbdb0 | ||
|
|
9678b8fd79 | ||
|
|
8e0c911f93 | ||
|
|
a2f3ec94aa | ||
|
|
38ccd7d5fc | ||
|
|
4faf89fc6f | ||
|
|
5b32a990a2 | ||
|
|
cb6daa5e4c | ||
|
|
d68347f5fb | ||
|
|
e676c8e6e1 | ||
|
|
34fa9138d9 | ||
|
|
7a917968e3 | ||
|
|
5f548243e1 | ||
|
|
34d22e9495 | ||
|
|
bc4fc765b2 | ||
|
|
253a560b20 | ||
|
|
eb3d2c6676 | ||
|
|
b1c6994977 | ||
|
|
2990762df9 | ||
|
|
8e6af6492b | ||
|
|
c4a606112a | ||
|
+6 |
230dc6fd05 | ||
|
|
e4b7002909 | ||
|
|
02c6427823 | ||
|
|
50b0ccb928 | ||
|
|
1712b9cdc5 | ||
|
|
b201ed163e | ||
|
|
3521ccfb4f | ||
|
|
b7cdfb1b30 | ||
|
|
4b1ae0526b | ||
|
|
378e734eed | ||
|
|
ed3b5305f1 | ||
|
|
cfc69d8d0b | ||
|
|
2a1859d7d7 | ||
|
|
8a2a2b1f36 | ||
|
|
af04276bed | ||
|
|
25d4bf5e2f | ||
|
|
d75db63258 | ||
|
|
52fc38e965 | ||
|
|
580863e891 | ||
|
|
85eb539c4d | ||
|
|
09ee38bb7a | ||
|
|
af4f17efc5 | ||
|
|
fa197a4ab2 | ||
|
|
c3243882b1 | ||
|
|
a938708e1f | ||
|
|
2e635ff2ce | ||
|
|
209805359e | ||
|
|
eb5b762515 | ||
|
|
7c5034275f | ||
|
|
d75a125b26 | ||
|
|
54d2c7b9e0 | ||
|
|
8b9d0a89c8 | ||
|
|
3bde967291 | ||
|
|
b40f1d3975 | ||
|
|
42daa3f432 | ||
|
|
f8e82b9322 | ||
|
|
cb0e329716 | ||
|
|
34afdabcfe | ||
|
|
bc32308327 | ||
|
|
c95269afd7 | ||
|
|
4af8f1eab0 | ||
|
+2 |
2366fbb4b3 | ||
|
|
e930f38608 | ||
|
|
b58d2a5d4b | ||
|
|
d3dcb7fe4b | ||
|
|
56b70ef529 | ||
|
|
5cf305b620 | ||
|
|
87665736a9 | ||
|
|
2f9edb551f | ||
|
|
d4b88b13b8 | ||
|
|
99edfbe4a8 | ||
|
|
fa7155896c | ||
|
|
51fb9c0b76 | ||
|
|
c495753315 | ||
|
|
11ced33fdb | ||
|
|
8c480c6355 | ||
|
|
e9ca71d837 | ||
|
|
e4091c0160 | ||
|
|
a2d22a7d55 | ||
|
|
db22517339 | ||
|
|
33faa5a356 | ||
|
|
8c5ae44f51 | ||
|
|
575d7e8b89 | ||
|
|
8c00b44eda | ||
|
|
77a86d486f | ||
|
|
4b379576f4 | ||
|
|
be88ce199c | ||
|
|
32b8ef6240 | ||
|
|
f82b8300b0 | ||
|
|
ce5820b496 | ||
|
|
f6b2684323 | ||
|
|
0d768af3d0 | ||
|
|
08f0bcc0d8 | ||
|
|
a3c45a05b8 | ||
|
|
6926914385 | ||
|
|
f3d5491a7d | ||
|
|
81871a3469 | ||
|
|
0024278f4b | ||
|
|
f9f938f5af | ||
|
|
1d7c838ae6 | ||
|
|
d219c5e3bb | ||
|
|
cc3f640faa | ||
|
|
84265febf3 | ||
|
|
171a06a8ba | ||
|
|
565379779f | ||
|
|
0394714274 | ||
|
|
75b445fa8f | ||
|
|
0cc724108b | ||
|
|
1e6c35d887 | ||
|
|
5b85084d0a | ||
|
|
8b2d35f306 | ||
|
|
47816d4b21 | ||
|
|
89bbdb17fb | ||
|
|
3c6f0f1697 | ||
|
|
ab214526c6 | ||
|
|
e6cc38de06 | ||
|
+12 |
7c7d35e2c6 | ||
|
|
9867c160f7 | ||
|
|
20c899f2cd | ||
|
|
3f64529412 | ||
|
|
27749fc583 | ||
|
|
2248bdac54 | ||
|
|
42c7329a9e | ||
|
|
22d5c9d9f4 | ||
|
|
5be5a4e819 | ||
|
|
b924d582f4 | ||
|
|
6f601a18c5 | ||
|
|
1ff9a5625c | ||
|
|
cae9fbf321 | ||
|
|
d17976c910 | ||
|
|
b609166702 | ||
|
|
cf93afab45 | ||
|
|
b543bc089a | ||
|
|
4e4bdffdf3 | ||
|
|
100cea0757 | ||
|
|
86344e20d3 | ||
|
|
e092b4208a | ||
|
|
1bf4eff931 | ||
|
|
b70edfac58 | ||
|
|
9c0eebb55c | ||
|
|
f6f1d13add | ||
|
|
31a9ff261e | ||
|
|
c0a1203541 | ||
|
|
82ffc8efa6 | ||
|
|
d6e17b04c3 | ||
|
|
e6c6700351 | ||
|
|
ede2f56bc6 | ||
|
|
2037ec4500 | ||
|
|
533a578bdb | ||
|
|
fa966fcb97 | ||
|
|
05d90ea4d6 | ||
|
|
49c4d08b22 | ||
|
|
bbe9aa8561 | ||
|
|
13975d6f7e | ||
|
|
e659e90c26 | ||
|
|
99e6fa3c06 | ||
|
|
cf13012629 | ||
|
|
93b355a9ef | ||
|
|
a4f5a8184c | ||
|
|
4ce249c1a0 | ||
|
|
edcf84d902 | ||
|
|
bd5c4d48f9 | ||
|
|
51b3ab3fd1 | ||
|
|
e6f72000ba | ||
|
|
9bf2d78a42 | ||
|
|
89c4e3bb39 | ||
|
|
47fe792ddc | ||
|
|
906d6f3cdb | ||
|
|
e059190fab | ||
|
|
a39b5a56e8 | ||
|
|
3e6afee13b | ||
|
|
9a11ec8ead | ||
|
|
1263df9d41 | ||
|
|
c96b6ae562 | ||
|
|
23c427cf60 | ||
|
|
e3260d56f7 | ||
|
|
f37afbcec9 | ||
|
|
7a1c8a1b61 | ||
|
|
532b5cf290 | ||
|
|
3cb1b2e17a | ||
|
|
4c9cfd8da5 | ||
|
|
2ecb70a590 | ||
|
|
2e0786f699 | ||
|
|
e7e4d9b6b3 | ||
|
|
5fe7dd9f06 | ||
|
|
4cb05cc738 | ||
|
|
876c3f951b | ||
|
|
08a61a42e9 | ||
|
|
cadd36ad9a | ||
|
|
97327555b3 | ||
|
|
5b88f88198 | ||
|
|
556371e1c8 | ||
|
|
c222a28bd1 | ||
|
|
7161bc2e82 | ||
|
|
c3d858a561 | ||
|
|
13552c5ffa | ||
|
|
e6ca458e1f | ||
|
|
05c7df2ed3 | ||
|
|
4a3339a21f | ||
|
|
6f497ed031 | ||
|
|
8b862c2e46 | ||
|
|
fc2c8c06a9 | ||
|
|
c17fc6792a | ||
|
|
9e27ae3d2b | ||
|
|
8b5dd39f1c | ||
|
|
8d81c94679 | ||
|
|
e5a693f224 | ||
|
|
7947e1bd6a | ||
|
|
cab729c939 | ||
|
|
f6a95a7416 | ||
|
|
32db0a5f4c | ||
|
|
2ec67ac0c1 | ||
|
|
c2bc95191b | ||
|
|
b1b79a63f0 | ||
|
|
ec99ab3aef | ||
|
|
69e5a1aea7 | ||
|
+2 |
84d620978b | ||
|
|
412815af06 | ||
|
|
f7fb68692a | ||
|
|
aa300cb53e | ||
|
|
855eea2ada | ||
|
|
f4703ed83a | ||
|
|
506d51a007 | ||
|
|
9f9155121c | ||
|
|
282110ef21 | ||
|
|
ace387f8bf | ||
|
|
5e428071c9 | ||
|
|
0acd80dd95 | ||
|
|
bdb0ce4779 | ||
|
|
e8bdf58530 |
@@ -29,3 +29,4 @@ ktlint_standard_function-expression-body = disabled
|
|||||||
ktlint_standard_function-signature = disabled
|
ktlint_standard_function-signature = disabled
|
||||||
ktlint_standard_type-argument-comment = disabled
|
ktlint_standard_type-argument-comment = disabled
|
||||||
ktlint_standard_type-parameter-comment = disabled
|
ktlint_standard_type-parameter-comment = disabled
|
||||||
|
ktlint_standard_blank-line-between-when-conditions = disabled
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
21
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
patreon: mihon
|
||||||
@@ -30,7 +30,7 @@ body:
|
|||||||
required: true
|
required: true
|
||||||
- label: I have written a short but informative title.
|
- label: I have written a short but informative title.
|
||||||
required: true
|
required: true
|
||||||
- label: I have updated the app to version **[0.19.2](https://github.com/mihonapp/mihon/releases/latest)**.
|
- label: I have updated the app to version **[0.21.2](https://github.com/mihonapp/mihon/releases/latest)**.
|
||||||
required: true
|
required: true
|
||||||
- label: I will fill out all of the requested information in this form.
|
- label: I will fill out all of the requested information in this form.
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ body:
|
|||||||
label: Mihon version
|
label: Mihon version
|
||||||
description: You can find your Mihon version in **More → About**.
|
description: You can find your Mihon version in **More → About**.
|
||||||
placeholder: |
|
placeholder: |
|
||||||
Example: "0.19.2"
|
Example: "0.21.2"
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
@@ -95,7 +95,7 @@ body:
|
|||||||
required: true
|
required: true
|
||||||
- label: I have gone through the [FAQ](https://mihon.app/docs/faq/general) and [troubleshooting guide](https://mihon.app/docs/guides/troubleshooting/).
|
- label: I have gone through the [FAQ](https://mihon.app/docs/faq/general) and [troubleshooting guide](https://mihon.app/docs/guides/troubleshooting/).
|
||||||
required: true
|
required: true
|
||||||
- label: I have updated the app to version **[0.19.2](https://github.com/mihonapp/mihon/releases/latest)**.
|
- label: I have updated the app to version **[0.21.2](https://github.com/mihonapp/mihon/releases/latest)**.
|
||||||
required: true
|
required: true
|
||||||
- label: I have filled out all of the requested information in this form, including specific version numbers.
|
- label: I have filled out all of the requested information in this form, including specific version numbers.
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
+27
-13
@@ -4,6 +4,7 @@ on:
|
|||||||
paths:
|
paths:
|
||||||
- '**'
|
- '**'
|
||||||
- '!**.md'
|
- '!**.md'
|
||||||
|
- '!fastlane/**'
|
||||||
- '!i18n/src/commonMain/moko-resources/**/strings.xml'
|
- '!i18n/src/commonMain/moko-resources/**/strings.xml'
|
||||||
- '!i18n/src/commonMain/moko-resources/**/plurals.xml'
|
- '!i18n/src/commonMain/moko-resources/**/plurals.xml'
|
||||||
- 'i18n/src/commonMain/moko-resources/base/strings.xml'
|
- 'i18n/src/commonMain/moko-resources/base/strings.xml'
|
||||||
@@ -22,42 +23,55 @@ permissions:
|
|||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: Build & Test App
|
name: Build & Test App
|
||||||
runs-on: 'ubuntu-24.04'
|
runs-on: 'ubuntu-26.04'
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
|
||||||
- name: Dependency Review
|
- name: Dependency Review
|
||||||
if: github.event_name == 'pull_request'
|
if: github.event_name == 'pull_request'
|
||||||
uses: actions/dependency-review-action@40c09b7dc99638e5ddb0bfd91c1673effc064d8a # v4.8.1
|
uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0
|
||||||
|
|
||||||
- name: Set up JDK
|
- name: Set up JDK
|
||||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1
|
||||||
with:
|
with:
|
||||||
java-version: 17
|
java-version-file: .github/.java-version
|
||||||
distribution: temurin
|
distribution: temurin
|
||||||
|
|
||||||
- name: Set up Gradle
|
- name: Set up Gradle
|
||||||
uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
|
uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0
|
||||||
|
with:
|
||||||
|
cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
|
||||||
|
|
||||||
- name: Check code format
|
- name: Check code format
|
||||||
run: ./gradlew spotlessCheck
|
run: ./gradlew spotlessCheck
|
||||||
|
|
||||||
- name: Build app
|
|
||||||
run: ./gradlew assembleRelease -Pinclude-telemetry -Penable-updater
|
|
||||||
|
|
||||||
- name: Run unit tests
|
- name: Run unit tests
|
||||||
run: ./gradlew testReleaseUnitTest
|
id: unit_tests
|
||||||
|
run: ./gradlew testDebugUnitTest
|
||||||
|
|
||||||
|
- name: Verify SQLDelight migrations
|
||||||
|
run: ./gradlew verifySqlDelightMigration
|
||||||
|
|
||||||
|
- name: Upload test report
|
||||||
|
if: steps.unit_tests.outcome == 'failure'
|
||||||
|
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||||
|
with:
|
||||||
|
name: test-report-${{ github.sha }}
|
||||||
|
path: app/build/reports/tests/testDebugUnitTest
|
||||||
|
|
||||||
|
- name: Build app
|
||||||
|
run: ./gradlew assembleRelease -Pdist=ci
|
||||||
|
|
||||||
- name: Upload APK
|
- name: Upload APK
|
||||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||||
with:
|
with:
|
||||||
name: arm64-v8a-${{ github.sha }}
|
name: arm64-v8a-${{ github.sha }}
|
||||||
path: app/build/outputs/apk/release/app-arm64-v8a-release-unsigned.apk
|
path: app/build/outputs/apk/release/app-arm64-v8a-release.apk
|
||||||
|
|
||||||
- name: Upload mapping
|
- name: Upload mapping
|
||||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||||
with:
|
with:
|
||||||
name: mapping-${{ github.sha }}
|
name: mapping-${{ github.sha }}
|
||||||
path: app/build/outputs/mapping/release
|
path: app/build/outputs/mapping/release
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ on:
|
|||||||
tags:
|
tags:
|
||||||
- v*
|
- v*
|
||||||
|
|
||||||
|
env:
|
||||||
|
MIHON_GITHUB_RELEASE: true
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
@@ -12,7 +15,7 @@ jobs:
|
|||||||
get_tag:
|
get_tag:
|
||||||
if: github.repository == 'mihonapp/mihon'
|
if: github.repository == 'mihonapp/mihon'
|
||||||
name: Extract tag name
|
name: Extract tag name
|
||||||
runs-on: 'ubuntu-24.04'
|
runs-on: 'ubuntu-26.04'
|
||||||
outputs:
|
outputs:
|
||||||
tag: ${{ steps.extract.outputs.tag }}
|
tag: ${{ steps.extract.outputs.tag }}
|
||||||
|
|
||||||
@@ -24,48 +27,49 @@ jobs:
|
|||||||
build:
|
build:
|
||||||
if: github.repository == 'mihonapp/mihon'
|
if: github.repository == 'mihonapp/mihon'
|
||||||
name: Build
|
name: Build
|
||||||
runs-on: 'ubuntu-24.04'
|
runs-on: 'ubuntu-26.04'
|
||||||
needs: get_tag
|
needs: get_tag
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
|
||||||
- name: Set up JDK
|
- name: Set up JDK
|
||||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1
|
||||||
with:
|
with:
|
||||||
java-version: 17
|
java-version-file: .github/.java-version
|
||||||
distribution: temurin
|
distribution: temurin
|
||||||
|
|
||||||
- name: Set up Gradle
|
- name: Set up Gradle
|
||||||
uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
|
uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0
|
||||||
|
with:
|
||||||
|
cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
|
||||||
|
|
||||||
|
- name: Decode signing keystore
|
||||||
|
run: echo "$SIGNING_KEY" | base64 -d > "${{ runner.temp }}/antsy.keystore"
|
||||||
|
env:
|
||||||
|
SIGNING_KEY: ${{ secrets.SIGNING_KEY }}
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: ./gradlew assembleRelease -Pinclude-telemetry -Penable-updater
|
run: ./gradlew assembleRelease -Pdist=github
|
||||||
|
|
||||||
- name: Sign APK
|
|
||||||
uses: r0adkll/sign-android-release@f30bdd30588842ac76044ecdbd4b6d0e3e813478
|
|
||||||
with:
|
|
||||||
releaseDirectory: app/build/outputs/apk/release
|
|
||||||
signingKeyBase64: ${{ secrets.SIGNING_KEY }}
|
|
||||||
alias: ${{ secrets.ALIAS }}
|
|
||||||
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
|
|
||||||
keyPassword: ${{ secrets.KEY_PASSWORD }}
|
|
||||||
env:
|
env:
|
||||||
BUILD_TOOLS_VERSION: '35.0.1'
|
storeFile: ${{ runner.temp }}/antsy.keystore
|
||||||
|
storePassword: ${{ secrets.KEY_STORE_PASSWORD }}
|
||||||
|
keyAlias: ${{ secrets.ALIAS }}
|
||||||
|
keyPassword: ${{ secrets.KEY_PASSWORD }}
|
||||||
|
|
||||||
- name: Rename APK
|
- name: Rename APK
|
||||||
run: |
|
run: |
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
mv app/build/outputs/apk/release/app-universal-release-unsigned-signed.apk mihon-${{ needs.get_tag.outputs.tag }}.apk
|
mv app/build/outputs/apk/release/app-universal-release.apk mihon-${{ needs.get_tag.outputs.tag }}.apk
|
||||||
mv app/build/outputs/apk/release/app-arm64-v8a-release-unsigned-signed.apk mihon-arm64-v8a-${{ needs.get_tag.outputs.tag }}.apk
|
mv app/build/outputs/apk/release/app-arm64-v8a-release.apk mihon-arm64-v8a-${{ needs.get_tag.outputs.tag }}.apk
|
||||||
mv app/build/outputs/apk/release/app-armeabi-v7a-release-unsigned-signed.apk mihon-armeabi-v7a-${{ needs.get_tag.outputs.tag }}.apk
|
mv app/build/outputs/apk/release/app-armeabi-v7a-release.apk mihon-armeabi-v7a-${{ needs.get_tag.outputs.tag }}.apk
|
||||||
mv app/build/outputs/apk/release/app-x86-release-unsigned-signed.apk mihon-x86-${{ needs.get_tag.outputs.tag }}.apk
|
mv app/build/outputs/apk/release/app-x86-release.apk mihon-x86-${{ needs.get_tag.outputs.tag }}.apk
|
||||||
mv app/build/outputs/apk/release/app-x86_64-release-unsigned-signed.apk mihon-x86_64-${{ needs.get_tag.outputs.tag }}.apk
|
mv app/build/outputs/apk/release/app-x86_64-release.apk mihon-x86_64-${{ needs.get_tag.outputs.tag }}.apk
|
||||||
|
|
||||||
- name: Upload APK
|
- name: Upload APK
|
||||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||||
with:
|
with:
|
||||||
name: mihon
|
name: mihon
|
||||||
path: |
|
path: |
|
||||||
@@ -78,46 +82,45 @@ jobs:
|
|||||||
build_foss:
|
build_foss:
|
||||||
if: github.repository == 'mihonapp/mihon'
|
if: github.repository == 'mihonapp/mihon'
|
||||||
name: Build (FOSS)
|
name: Build (FOSS)
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-26.04
|
||||||
needs: get_tag
|
needs: get_tag
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
|
||||||
- name: Set up JDK
|
- name: Set up JDK
|
||||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1
|
||||||
with:
|
with:
|
||||||
java-version: 17
|
java-version-file: .github/.java-version
|
||||||
distribution: temurin
|
distribution: temurin
|
||||||
|
|
||||||
- name: Set up Gradle
|
- name: Set up Gradle
|
||||||
uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
|
uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0
|
||||||
with:
|
with:
|
||||||
cache-disabled: true
|
cache-disabled: true
|
||||||
|
|
||||||
- name: Build
|
- name: Decode signing keystore
|
||||||
run: ./gradlew assembleFoss -Penable-updater
|
run: echo "$SIGNING_KEY" | base64 -d > "${{ runner.temp }}/antsy.keystore"
|
||||||
|
|
||||||
- name: Sign APK
|
|
||||||
uses: r0adkll/sign-android-release@f30bdd30588842ac76044ecdbd4b6d0e3e813478
|
|
||||||
with:
|
|
||||||
releaseDirectory: app/build/outputs/apk/foss
|
|
||||||
signingKeyBase64: ${{ secrets.SIGNING_KEY }}
|
|
||||||
alias: ${{ secrets.ALIAS }}
|
|
||||||
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
|
|
||||||
keyPassword: ${{ secrets.KEY_PASSWORD }}
|
|
||||||
env:
|
env:
|
||||||
BUILD_TOOLS_VERSION: '35.0.1'
|
SIGNING_KEY: ${{ secrets.SIGNING_KEY }}
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: ./gradlew assembleFoss -Pdist=foss
|
||||||
|
env:
|
||||||
|
storeFile: ${{ runner.temp }}/antsy.keystore
|
||||||
|
storePassword: ${{ secrets.KEY_STORE_PASSWORD }}
|
||||||
|
keyAlias: ${{ secrets.ALIAS }}
|
||||||
|
keyPassword: ${{ secrets.KEY_PASSWORD }}
|
||||||
|
|
||||||
- name: Rename APK
|
- name: Rename APK
|
||||||
run: |
|
run: |
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
mv app/build/outputs/apk/foss/app-universal-foss-unsigned-signed.apk mihon-${{ needs.get_tag.outputs.tag }}-foss.apk
|
mv app/build/outputs/apk/foss/app-universal-foss.apk mihon-${{ needs.get_tag.outputs.tag }}-foss.apk
|
||||||
|
|
||||||
- name: Upload APK
|
- name: Upload APK
|
||||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||||
with:
|
with:
|
||||||
name: mihon-foss
|
name: mihon-foss
|
||||||
path: mihon-${{ needs.get_tag.outputs.tag }}-foss.apk
|
path: mihon-${{ needs.get_tag.outputs.tag }}-foss.apk
|
||||||
@@ -125,17 +128,17 @@ jobs:
|
|||||||
release:
|
release:
|
||||||
if: github.repository == 'mihonapp/mihon'
|
if: github.repository == 'mihonapp/mihon'
|
||||||
name: Create GitHub Release
|
name: Create GitHub Release
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-26.04
|
||||||
needs: [get_tag, build, build_foss]
|
needs: [get_tag, build, build_foss]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Download all artifacts
|
- name: Download all artifacts
|
||||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
||||||
with:
|
with:
|
||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
|
|
||||||
- name: Delete all artifacts
|
- name: Delete all artifacts
|
||||||
uses: geekyeggo/delete-artifact@f275313e70c08f6120db482d7a6b98377786765b # v5.1.0
|
uses: geekyeggo/delete-artifact@176a747ab7e287e3ff4787bf8a148716375ca118 # v6.0.0
|
||||||
with:
|
with:
|
||||||
failOnError: false
|
failOnError: false
|
||||||
name: |
|
name: |
|
||||||
@@ -143,12 +146,14 @@ jobs:
|
|||||||
mihon-foss
|
mihon-foss
|
||||||
|
|
||||||
- name: Create GitHub Release
|
- name: Create GitHub Release
|
||||||
uses: softprops/action-gh-release@6da8fa9354ddfdc4aeace5fc48d7f679b5214090 # v2.4.1
|
uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3.0.3
|
||||||
with:
|
with:
|
||||||
tag_name: ${{ needs.get_tag.outputs.tag }}
|
tag_name: ${{ needs.get_tag.outputs.tag }}
|
||||||
name: Mihon ${{ needs.get_tag.outputs.tag }}
|
name: Mihon ${{ needs.get_tag.outputs.tag }}
|
||||||
body: |
|
body: |
|
||||||
Check out the [past release notes](https://github.com/mihonapp/mihon/releases) if you’re upgrading from an earlier version. Consider [donating via Open Collective](https://opencollective.com/mihon/contribute) to help keep Mihon improving!
|
Consider supporting the development on [Patreon](https://www.patreon.com/mihon/membership) or [OpenCollective](https://opencollective.com/mihon/contribute) to keep Mihon improving!
|
||||||
|
|
||||||
|
Check out the [past release notes](https://github.com/mihonapp/mihon/releases) if you're upgrading from an earlier version.
|
||||||
|
|
||||||
<!-->
|
<!-->
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
update_website:
|
update_website:
|
||||||
runs-on: 'ubuntu-24.04'
|
runs-on: 'ubuntu-26.04'
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Update website
|
- name: Update website
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ build
|
|||||||
/captures
|
/captures
|
||||||
|
|
||||||
# Configuration files
|
# Configuration files
|
||||||
|
keystore.properties
|
||||||
local.properties
|
local.properties
|
||||||
|
|
||||||
# macOS specific files
|
# macOS specific files
|
||||||
|
|||||||
+370
-2
@@ -12,6 +12,359 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [v0.21.2] - 2026-10-13
|
||||||
|
### Fixed
|
||||||
|
- Fix long floating point numbers showing up in Comick progress ([@MajorTanya](https://github.com/MajorTanya)) ([#4090](https://github.com/mihonapp/mihon/pull/4090))
|
||||||
|
- Fixed updating from v0.20.x failing when the database had duplicate history or tracking entries, or damaged indexes ([@AntsyLich](https://github.com/AntsyLich)) ([`ea2b73a`](https://github.com/mihonapp/mihon/commit/ea2b73a))
|
||||||
|
- Fixed a crash when a back gesture was still in progress as the reader or a dialog closed ([@AntsyLich](https://github.com/AntsyLich)) ([`43de88c`](https://github.com/mihonapp/mihon/commit/43de88c))
|
||||||
|
- Fixed Kitsu login and other requests that send data failing with "Failed to bypass Cloudflare" ([@AntsyLich](https://github.com/AntsyLich)) ([`7ae858e`](https://github.com/mihonapp/mihon/commit/7ae858e))
|
||||||
|
- Fixed tracker start date being off by a day when adding tracking to an entry with read history in some timezones ([@hazptr](https://github.com/hazptr)) ([#4049](https://github.com/mihonapp/mihon/pull/4049))
|
||||||
|
- Fixed the paged reader crashing when moving to another chapter after a wide page of a neighbouring chapter was split ([@AntsyLich](https://github.com/AntsyLich)) ([`3156b4a`](https://github.com/mihonapp/mihon/commit/3156b4a))
|
||||||
|
- Fixed a crash when opening the filters of a source with an invalid filter ([@AntsyLich](https://github.com/AntsyLich)) ([`c9e1b6e`](https://github.com/mihonapp/mihon/commit/c9e1b6e))
|
||||||
|
- Fixed a crash when installing an extension on a device where the system installer can't ask for confirmation ([@AntsyLich](https://github.com/AntsyLich)) ([`2df48e1`](https://github.com/mihonapp/mihon/commit/2df48e1))
|
||||||
|
- Fixed a crash when tapping in the long strip reader as it closed ([@AntsyLich](https://github.com/AntsyLich)) ([`c371d63`](https://github.com/mihonapp/mihon/commit/c371d63))
|
||||||
|
- Fixed chapters downloaded before v0.21.0 showing as not downloaded when their name started with a space or dot, was long, or had a non-ASCII scanlator ([@AntsyLich](https://github.com/AntsyLich)) ([`f0f1e4d`](https://github.com/mihonapp/mihon/commit/f0f1e4d))
|
||||||
|
- Fixed slowdowns and running out of memory when many requests to a site hit a Cloudflare challenge at once ([@tigeryu8900](https://github.com/tigeryu8900), [@AntsyLich](https://github.com/AntsyLich)) ([`5a5ff22`](https://github.com/mihonapp/mihon/commit/5a5ff22))
|
||||||
|
|
||||||
|
## [v0.21.1] - 2026-10-12
|
||||||
|
### Fixed
|
||||||
|
- Fixed refreshing chapters failing with the app language set to Arabic or another language with its own digits ([@AntsyLich](https://github.com/AntsyLich)) ([`8457fb9`](https://github.com/mihonapp/mihon/commit/8457fb9))
|
||||||
|
- Fixed a crash on launch on some Samsung devices running Android 9 to 11 ([@AntsyLich](https://github.com/AntsyLich)) ([`abb126a`](https://github.com/mihonapp/mihon/commit/abb126a))
|
||||||
|
- Fixed the start and resume button going under the taskbar on tablets ([@AntsyLich](https://github.com/AntsyLich)) ([`c279291`](https://github.com/mihonapp/mihon/commit/c279291))
|
||||||
|
- Fixed a crash when updating an extension while others were installing ([@AntsyLich](https://github.com/AntsyLich)) ([`07b9fb6`](https://github.com/mihonapp/mihon/commit/07b9fb6))
|
||||||
|
- Fixed library entries without a date added getting their last update time as date added when updating from v0.20.x ([@AntsyLich](https://github.com/AntsyLich)) ([`6dffca1`](https://github.com/mihonapp/mihon/commit/6dffca1))
|
||||||
|
- Fixed the app closing when WebView's page process crashed ([@AntsyLich](https://github.com/AntsyLich)) ([`649fc83`](https://github.com/mihonapp/mihon/commit/649fc83))
|
||||||
|
- Fixed installing or updating an extension sometimes cancelling the install of another one ([@AntsyLich](https://github.com/AntsyLich)) ([`ae749bb`](https://github.com/mihonapp/mihon/commit/ae749bb))
|
||||||
|
- Fixed updating from v0.20.x taking up to hours on libraries with many duplicate entries ([@AntsyLich](https://github.com/AntsyLich)) ([`a999e49`](https://github.com/mihonapp/mihon/commit/a999e49))
|
||||||
|
- Fixed the app staying on the updating screen for good when updating its data failed, it now shows what went wrong, and updating failing on leftover data of deleted entries ([@AntsyLich](https://github.com/AntsyLich)) ([`9dbb714`](https://github.com/mihonapp/mihon/commit/9dbb714))
|
||||||
|
- Fixed library updates ignoring the smart update restrictions for entries whose details were never fetched ([@AntsyLich](https://github.com/AntsyLich)) ([`010ac71`](https://github.com/mihonapp/mihon/commit/010ac71))
|
||||||
|
- Fixed resuming a chapter sometimes opening the next chapter partway through, and saving progress to it, in the right to left reader ([@AntsyLich](https://github.com/AntsyLich)) ([`51b61a3`](https://github.com/mihonapp/mihon/commit/51b61a3))
|
||||||
|
|
||||||
|
## [v0.21.0] - 2026-10-12
|
||||||
|
### Added
|
||||||
|
- Add `id:` prefix search to remaining trackers (AniList, Bangumi, Kitsu, MangaUpdates, Shikimori, and Hikka) ([@MajorTanya](https://github.com/MajorTanya), [@BrutuZ](https://github.com/BrutuZ)) ([#3776](https://github.com/mihonapp/mihon/pull/3776))
|
||||||
|
- Allow `id:` to search for slugs on Kitsu ([@MajorTanya](https://github.com/MajorTanya)) ([#3792](https://github.com/mihonapp/mihon/pull/3792))
|
||||||
|
- Add support for using the user's chosen rating system for Kitsu ([@MajorTanya](https://github.com/MajorTanya)) ([#3818](https://github.com/mihonapp/mihon/pull/3818))
|
||||||
|
- Fix a crash after logging in to Kitsu caused by the rating system's letter case ([@Secozzi](https://github.com/Secozzi)) ([#3887](https://github.com/mihonapp/mihon/pull/3887))
|
||||||
|
- Add option to keep loading installed extensions regardless of the allowed content warnings ([@AntsyLich](https://github.com/AntsyLich)) ([#3952](https://github.com/mihonapp/mihon/pull/3952))
|
||||||
|
- Read chapter dates from the Year, Month, and Day fields of ComicInfo.xml in local source, and write them when downloading chapters ([@MajorTanya](https://github.com/MajorTanya)) ([#3967](https://github.com/mihonapp/mihon/pull/3967))
|
||||||
|
- Fix wrongly padded years and months above 12 in local source dates ([@MajorTanya](https://github.com/MajorTanya)) ([#3970](https://github.com/mihonapp/mihon/pull/3970))
|
||||||
|
- Add refresh buttons to trackers in Settings to update displayed usernames & rating systems (where supported) ([@MajorTanya](https://github.com/MajorTanya)) ([#3828](https://github.com/mihonapp/mihon/pull/3828))
|
||||||
|
- Add option to turn off the hash suffix in downloaded chapter folder names (Settings → Advanced). It stays on for existing installs ([@choppeh](https://github.com/choppeh), [@AntsyLich](https://github.com/AntsyLich)) ([#3966](https://github.com/mihonapp/mihon/pull/3966))
|
||||||
|
- Fixed downloads not being found when the scanlator name contains a "/" ([@AntsyLich](https://github.com/AntsyLich)) ([`a5d584e`](https://github.com/mihonapp/mihon/commit/a5d584e))
|
||||||
|
- Show a screen while the app migrates its data after an update ([@AntsyLich](https://github.com/AntsyLich)) ([`fd4f73c`](https://github.com/mihonapp/mihon/commit/fd4f73c))
|
||||||
|
- Keep the screen up for at least a second so it doesn't just flash by ([@AntsyLich](https://github.com/AntsyLich)) ([`b454c1b`](https://github.com/mihonapp/mihon/commit/b454c1b))
|
||||||
|
- Add option to skip an app update version ([@AntsyLich](https://github.com/AntsyLich)) ([`fc20410`](https://github.com/mihonapp/mihon/commit/fc20410))
|
||||||
|
- Add support for [Comick](https://comick.dev) tracker ([@MajorTanya](https://github.com/MajorTanya)) ([#4056](https://github.com/mihonapp/mihon/pull/4056))
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Download app updates from the update screen instead of a background job, and remove the "App updates" notification channel ([@AntsyLich](https://github.com/AntsyLich)) ([#3707](https://github.com/mihonapp/mihon/pull/3707))
|
||||||
|
- Switch app icons to Material Symbols ([@AntsyLich](https://github.com/AntsyLich)) ([#3873](https://github.com/mihonapp/mihon/pull/3873))
|
||||||
|
- Use the filled icon for the library continue reading button ([@AntsyLich](https://github.com/AntsyLich)) ([`34a33ed`](https://github.com/mihonapp/mihon/commit/34a33ed))
|
||||||
|
- Move notification, launcher shortcut, bottom navigation and reading mode icons to Material Symbols, bring back filled icons where the switch dropped them, and show a compact grid icon for the compact display modes ([@AntsyLich](https://github.com/AntsyLich)) ([#4057](https://github.com/mihonapp/mihon/pull/4057))
|
||||||
|
- Replace the NSFW sources toggle with allowed content warnings (Safe, Mixed, 18+) for extensions ([@AntsyLich](https://github.com/AntsyLich)) ([#3951](https://github.com/mihonapp/mihon/pull/3951))
|
||||||
|
- Restoring a backup over an existing library now keeps existing entry details, the furthest read progress and the earliest date added ([@AntsyLich](https://github.com/AntsyLich)) ([`c67a33f`](https://github.com/mihonapp/mihon/commit/c67a33f))
|
||||||
|
- Install and update extensions only from a repo whose signing key matches the extension, and reject downloaded extensions not signed with their repo's key ([@AntsyLich](https://github.com/AntsyLich)) ([`0938411`](https://github.com/mihonapp/mihon/commit/0938411))
|
||||||
|
- Keep the repo shown for an installed extension current when repos are renamed or removed ([@AntsyLich](https://github.com/AntsyLich)) ([`09edf7d`](https://github.com/mihonapp/mihon/commit/09edf7d))
|
||||||
|
- Show the repo name in extension details, opening the repo's website, instead of the "Open source repo" button ([@AntsyLich](https://github.com/AntsyLich)) ([#3955](https://github.com/mihonapp/mihon/pull/3955))
|
||||||
|
- Rework the database schema, merging duplicate entries and chapters left by older versions. The merge can't be undone, so create a backup before updating ([@AntsyLich](https://github.com/AntsyLich)) ([#3805](https://github.com/mihonapp/mihon/pull/3805))
|
||||||
|
- Fixed restoring a backup failing on entries that list a category or excluded scanlator twice ([@AntsyLich](https://github.com/AntsyLich)) ([#4022](https://github.com/mihonapp/mihon/pull/4022))
|
||||||
|
- List MangaBaka first among trackers ([@AntsyLich](https://github.com/AntsyLich)) ([`dd46413`](https://github.com/mihonapp/mihon/commit/dd46413))
|
||||||
|
- Show a wavy progress ring while a chapter downloads ([@AntsyLich](https://github.com/AntsyLich)) ([`e562a9b`](https://github.com/mihonapp/mihon/commit/e562a9b))
|
||||||
|
- Show a confirmation dialog when unfavoriting manga from the manga screen ([@WizaHX](https://github.com/WizaHX)) ([#4073](https://github.com/mihonapp/mihon/pull/4073))
|
||||||
|
|
||||||
|
### Improved
|
||||||
|
- Show the updates and upcoming filter icon as active when filtering by category, or by hidden excluded scanlators in updates ([@Secozzi](https://github.com/Secozzi)) ([#3772](https://github.com/mihonapp/mihon/pull/3772))
|
||||||
|
- Load extensions in the background to speed up app startup ([@AntsyLich](https://github.com/AntsyLich)) ([#3788](https://github.com/mihonapp/mihon/pull/3788))
|
||||||
|
- Wait for extensions to load before listing sources, so lists and global search aren't empty right after launch ([@AntsyLich](https://github.com/AntsyLich)) ([#3841](https://github.com/mihonapp/mihon/pull/3841))
|
||||||
|
- Show scores in MangaUpdates search results (and authors for `id:` prefix searches) ([@MajorTanya](https://github.com/MajorTanya)) ([#3795](https://github.com/mihonapp/mihon/pull/3795))
|
||||||
|
- Remove whitespace from MAL and MB `id:` prefix search inputs before searching ([@MajorTanya](https://github.com/MajorTanya)) ([#3793](https://github.com/mihonapp/mihon/pull/3793))
|
||||||
|
- Detect Cloudflare challenges with Cloudflare's `cf-mitigated` header, and fail sooner when a challenge needs user interaction instead of waiting for the timeout ([@tigeryu8900](https://github.com/tigeryu8900)) ([#3842](https://github.com/mihonapp/mihon/pull/3842))
|
||||||
|
- Show a helpful error message for expired AniList credentials ([@MajorTanya](https://github.com/MajorTanya)) ([#3888](https://github.com/mihonapp/mihon/pull/3888))
|
||||||
|
- Show installed extensions that couldn't be loaded, with the reason and a copyable stack trace ([@AntsyLich](https://github.com/AntsyLich)) ([#3953](https://github.com/mihonapp/mihon/pull/3953))
|
||||||
|
- Apply extension trust, repo and content warning changes without restarting the app ([@AntsyLich](https://github.com/AntsyLich)) ([#3954](https://github.com/mihonapp/mihon/pull/3954))
|
||||||
|
- Use the system document picker when selecting a backup to restore ([@MajorTanya](https://github.com/MajorTanya)) ([#3948](https://github.com/mihonapp/mihon/pull/3948))
|
||||||
|
- Offer updates for installed extensions that aren't loaded ([@AntsyLich](https://github.com/AntsyLich)) ([`2d86a70`](https://github.com/mihonapp/mihon/commit/2d86a70))
|
||||||
|
- Show available extension updates on the Browse badge and in the Extensions tab right after launch ([@AntsyLich](https://github.com/AntsyLich)) ([`4f33486`](https://github.com/mihonapp/mihon/commit/4f33486))
|
||||||
|
- Show which repo an extension comes from in the extensions list, and list an extension once for each repo signing key offering it ([@AntsyLich](https://github.com/AntsyLich)) ([#3955](https://github.com/mihonapp/mihon/pull/3955))
|
||||||
|
- Speed up loading the library on large libraries ([@AntsyLich](https://github.com/AntsyLich)) ([#3805](https://github.com/mihonapp/mihon/pull/3805))
|
||||||
|
- Speed up history, updates, the updates widget and browse on large libraries ([@AntsyLich](https://github.com/AntsyLich)) ([#4022](https://github.com/mihonapp/mihon/pull/4022))
|
||||||
|
- Reduce lag when dragging sliders in settings ([@AntsyLich](https://github.com/AntsyLich)) ([`9409740`](https://github.com/mihonapp/mihon/commit/9409740))
|
||||||
|
- Speed up refreshing the library when returning to it ([@AntsyLich](https://github.com/AntsyLich)) ([`f027de8`](https://github.com/mihonapp/mihon/commit/f027de8))
|
||||||
|
- Download a source's chapters faster by starting the next one while the current one finishes ([@AntsyLich](https://github.com/AntsyLich)) ([`730e43f`](https://github.com/mihonapp/mihon/commit/730e43f))
|
||||||
|
- Load an entry's cover once when several updates show it ([@AntsyLich](https://github.com/AntsyLich)) ([`1f85a5a`](https://github.com/mihonapp/mihon/commit/1f85a5a))
|
||||||
|
- Speed up the entry details chapter list on entries with many chapters, and the updates list when many chapters are queued for download ([@AntsyLich](https://github.com/AntsyLich)) ([`fb8e640`](https://github.com/mihonapp/mihon/commit/fb8e640))
|
||||||
|
- Speed up checking which chapters are downloaded ([@AntsyLich](https://github.com/AntsyLich)) ([`6a32938`](https://github.com/mihonapp/mihon/commit/6a32938))
|
||||||
|
- Stop the entry details screen from refreshing its chapter list in the background while reading ([@AntsyLich](https://github.com/AntsyLich)) ([`7ff9776`](https://github.com/mihonapp/mihon/commit/7ff9776))
|
||||||
|
- Speed up opening the library on large libraries by loading tracker entries only for tracker filters and sorting ([@AntsyLich](https://github.com/AntsyLich)) ([`940272f`](https://github.com/mihonapp/mihon/commit/940272f))
|
||||||
|
- Speed up the library query on large libraries ([@AntsyLich](https://github.com/AntsyLich)) ([`5df78e3`](https://github.com/mihonapp/mihon/commit/5df78e3))
|
||||||
|
- Speed up the statistics screen on large libraries ([@AntsyLich](https://github.com/AntsyLich)) ([`179bfac`](https://github.com/mihonapp/mihon/commit/179bfac))
|
||||||
|
- Find settings by their group heading in settings search ([@kaosjammo](https://github.com/kaosjammo), [@AntsyLich](https://github.com/AntsyLich)) ([#4044](https://github.com/mihonapp/mihon/pull/4044))
|
||||||
|
- Predict the next library update from release patterns such as fixed weekdays or a day of the month, ignore chapters dated in the future, and include the predicted date in backups ([@AntsyLich](https://github.com/AntsyLich)) ([`2923816`](https://github.com/mihonapp/mihon/commit/2923816))
|
||||||
|
- Also follow two days of the month, a weekday of the month, releases in batches and breaks between seasons, and never wait more than 28 days between checks ([@AntsyLich](https://github.com/AntsyLich)) ([`ba44815`](https://github.com/mihonapp/mihon/commit/ba44815))
|
||||||
|
- Recognize chapter numbers more accurately by looking at neighbouring chapters, and stop taking volume numbers or numbers every chapter name shares as the chapter number ([@AntsyLich](https://github.com/AntsyLich)) ([`e42e010`](https://github.com/mihonapp/mihon/commit/e42e010))
|
||||||
|
- Move blocking work off the main thread across the app for fewer freezes and stutters ([@AntsyLich](https://github.com/AntsyLich)) ([#4068](https://github.com/mihonapp/mihon/pull/4068))
|
||||||
|
- Load reader pages in parallel and preload the rest of the chapter and the next chapter, with a "Load ahead only on Wi-Fi" setting to save mobile data ([@AntsyLich](https://github.com/AntsyLich)) ([#2986](https://github.com/mihonapp/mihon/pull/2986))
|
||||||
|
- Cache extension and source icons so lists show them faster ([@AntsyLich](https://github.com/AntsyLich)) ([`b15af71`](https://github.com/mihonapp/mihon/commit/b15af71))
|
||||||
|
- Keep screens loaded while they're hidden so switching back to them is faster ([@AntsyLich](https://github.com/AntsyLich)) ([#4071](https://github.com/mihonapp/mihon/pull/4071))
|
||||||
|
- Speed up the migration list, the extensions list and settings search ([@AntsyLich](https://github.com/AntsyLich)) ([#4071](https://github.com/mihonapp/mihon/pull/4071))
|
||||||
|
- Cancel the previous tracker search when searching again ([@AntsyLich](https://github.com/AntsyLich)) ([#4071](https://github.com/mihonapp/mihon/pull/4071))
|
||||||
|
- Do less background work while reading, downloading and updating the library, and when no updates widget is placed ([@AntsyLich](https://github.com/AntsyLich)) ([#4071](https://github.com/mihonapp/mihon/pull/4071))
|
||||||
|
- Speed up loading extension repos at startup ([@AntsyLich](https://github.com/AntsyLich)) ([`8066b8d`](https://github.com/mihonapp/mihon/commit/8066b8d))
|
||||||
|
- Speed up opening the entry details screen and showing download progress there and in updates ([@AntsyLich](https://github.com/AntsyLich)) ([`ba16158`](https://github.com/mihonapp/mihon/commit/ba16158))
|
||||||
|
- Speed up queueing many chapters for download at once ([@AntsyLich](https://github.com/AntsyLich)) ([`f02bf5b`](https://github.com/mihonapp/mihon/commit/f02bf5b))
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
- Remove "Use legacy decoder for long strip reader" advanced setting, since long strip pages no longer load as one big image ([@AntsyLich](https://github.com/AntsyLich))
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Fixed app and extension update check running again on configuration change ([@AntsyLich](https://github.com/AntsyLich)) ([#3708](https://github.com/mihonapp/mihon/pull/3708))
|
||||||
|
- Fixed MangaBaka user start/finish dates drifting in negative offset timezones ([@MajorTanya](https://github.com/MajorTanya)) ([#3711](https://github.com/mihonapp/mihon/pull/3711))
|
||||||
|
- Fixed MangaBaka scores being wrong when score step size was set to > 1, and being counted 10 times too high in library sorting by tracker score and in statistics ([@MajorTanya](https://github.com/MajorTanya)) ([#3740](https://github.com/mihonapp/mihon/pull/3740))
|
||||||
|
- Fixed tracking an entry on Shikimori failing with "Unknown status: 0" when it isn't in your Shikimori list ([@MajorTanya](https://github.com/MajorTanya)) ([#3810](https://github.com/mihonapp/mihon/pull/3810))
|
||||||
|
- Fixed default category and manga sometimes not getting their category set when restoring a backup ([@Secozzi](https://github.com/Secozzi)) ([#3891](https://github.com/mihonapp/mihon/pull/3891))
|
||||||
|
- Fixed text between dollar signs disappearing from descriptions and notes ([@renovate](https://github.com/apps/renovate)) ([#3806](https://github.com/mihonapp/mihon/pull/3806))
|
||||||
|
- Fixed AniList requests failing with rate limit errors by lowering the request rate to AniList's current limit ([@MajorTanya](https://github.com/MajorTanya)) ([#3942](https://github.com/mihonapp/mihon/pull/3942))
|
||||||
|
- Fixed library search when clicking source name in manga info screen ([@choppeh](https://github.com/choppeh)) ([#4002](https://github.com/mihonapp/mihon/pull/4002))
|
||||||
|
- Fixed entries being incorrectly marked as duplicates when tracked with some trackers ([@Secozzi](https://github.com/Secozzi)) ([#4008](https://github.com/mihonapp/mihon/pull/4008))
|
||||||
|
- Fixed the default chapter settings not being applied to entries added to the library from a source by long pressing them ([@AntsyLich](https://github.com/AntsyLich)) ([`f8fff31`](https://github.com/mihonapp/mihon/commit/f8fff31))
|
||||||
|
- Fixed chapter list updates being partially applied when a refresh fails, and sources that list a chapter twice adding it twice ([@AntsyLich](https://github.com/AntsyLich)) ([`6ee529c`](https://github.com/mihonapp/mihon/commit/6ee529c))
|
||||||
|
- Fixed downloads of entries from uninstalled extensions sometimes not being recognized ([@AntsyLich](https://github.com/AntsyLich)) ([`0044cc7`](https://github.com/mihonapp/mihon/commit/0044cc7))
|
||||||
|
- Fixed a removed extension repo coming back when it was removed while repos were refreshing ([@AntsyLich](https://github.com/AntsyLich)) ([`fc5592f`](https://github.com/mihonapp/mihon/commit/fc5592f))
|
||||||
|
- Fixed being able to rename a category to an empty name ([@AntsyLich](https://github.com/AntsyLich)) ([`8bfa495`](https://github.com/mihonapp/mihon/commit/8bfa495))
|
||||||
|
- Fixed reading time sometimes being counted twice, and tracker start dates being set to 1970 after removing history ([@AntsyLich](https://github.com/AntsyLich)) ([`553762f`](https://github.com/mihonapp/mihon/commit/553762f))
|
||||||
|
- Fixed chapters from excluded scanlators failing to open from notifications or links ([@AntsyLich](https://github.com/AntsyLich)) ([`2b4b847`](https://github.com/mihonapp/mihon/commit/2b4b847))
|
||||||
|
- Fixed queued offline tracker updates being lost when the track was refreshed ([@AntsyLich](https://github.com/AntsyLich)) ([`9a77bae`](https://github.com/mihonapp/mihon/commit/9a77bae))
|
||||||
|
- Fixed restoring a backup mixing up tracker entries of an entry that was already tracked ([@AntsyLich](https://github.com/AntsyLich)) ([`4b48a84`](https://github.com/mihonapp/mihon/commit/4b48a84))
|
||||||
|
- Fixed crashes from APIs extensions use being removed from release builds ([@AntsyLich](https://github.com/AntsyLich)) ([`6dfa10f`](https://github.com/mihonapp/mihon/commit/6dfa10f))
|
||||||
|
- Fixed the download service staying alive after downloads stopped for a network change ([@AntsyLich](https://github.com/AntsyLich)) ([`01d302f`](https://github.com/mihonapp/mihon/commit/01d302f))
|
||||||
|
- Fixed "database is locked" crashes ([@AntsyLich](https://github.com/AntsyLich)) ([`38e9308`](https://github.com/mihonapp/mihon/commit/38e9308))
|
||||||
|
- Use WAL journaling on every device ([@AntsyLich](https://github.com/AntsyLich)) ([`61cd786`](https://github.com/mihonapp/mihon/commit/61cd786))
|
||||||
|
- Fixed global search leaking threads each time it was opened ([@choppeh](https://github.com/choppeh)) ([#4036](https://github.com/mihonapp/mihon/pull/4036))
|
||||||
|
- Fixed sources getting stuck loading in global and migration search ([@choppeh](https://github.com/choppeh)) ([#4037](https://github.com/mihonapp/mihon/pull/4037))
|
||||||
|
- Fixed webtoon reader leaving a gap after zooming in split screen or a resized window ([@AntsyLich](https://github.com/AntsyLich)) ([`d1c5c12`](https://github.com/mihonapp/mihon/commit/d1c5c12))
|
||||||
|
- Fixed download error notifications showing an old time and the previous warning's text ([@AntsyLich](https://github.com/AntsyLich)) ([`f82a7a6`](https://github.com/mihonapp/mihon/commit/f82a7a6))
|
||||||
|
- Fixed the paused downloads notification not appearing ([@AntsyLich](https://github.com/AntsyLich)) ([`7aacaa3`](https://github.com/mihonapp/mihon/commit/7aacaa3))
|
||||||
|
- Fixed duplicate page files and failed downloads after pausing and resuming downloads ([@AntsyLich](https://github.com/AntsyLich)) ([`6490c41`](https://github.com/mihonapp/mihon/commit/6490c41))
|
||||||
|
- Fixed high CPU use on the entry details and updates screens while many chapters are queued for download ([@AntsyLich](https://github.com/AntsyLich)) ([`9a6dea8`](https://github.com/mihonapp/mihon/commit/9a6dea8))
|
||||||
|
- Fixed backups dropping the names of sources whose extension isn't installed, which left them blank under missing sources when restoring ([@AntsyLich](https://github.com/AntsyLich)) ([`5910a51`](https://github.com/mihonapp/mihon/commit/5910a51))
|
||||||
|
- Fixed the updates widget showing "Can't show content" when there are no recent updates or app lock is on ([@AntsyLich](https://github.com/AntsyLich)) ([`76b146f`](https://github.com/mihonapp/mihon/commit/76b146f))
|
||||||
|
- Fixed a crash when range selecting library entries across categories ([@AntsyLich](https://github.com/AntsyLich)) ([`a6ee4bc`](https://github.com/mihonapp/mihon/commit/a6ee4bc))
|
||||||
|
- Fixed reader tap navigation not responding after a button press turned into a scroll ([@mujina3](https://github.com/mujina3)) ([#3655](https://github.com/mihonapp/mihon/pull/3655))
|
||||||
|
- Fixed editing the cover of an entry that isn't in the library saying the cover was updated without changing it, and say to add it to the library first instead ([@quickdesh](https://github.com/quickdesh), [@AntsyLich](https://github.com/AntsyLich)) ([#2265](https://github.com/mihonapp/mihon/pull/2265))
|
||||||
|
- Fixed downloads of duplicate chapters not being deleted when they were marked as read along with a chapter ([@leodyversemilla07](https://github.com/leodyversemilla07), [@AntsyLich](https://github.com/AntsyLich)) ([#3030](https://github.com/mihonapp/mihon/pull/3030))
|
||||||
|
- Fixed library updates skipping entries whose details were never fetched ([@NarwhalHorns](https://github.com/NarwhalHorns), [@AntsyLich](https://github.com/AntsyLich)) ([#3179](https://github.com/mihonapp/mihon/pull/3179))
|
||||||
|
- Fixed every library entry being matched as a duplicate of an entry with a blank title ([@NarwhalHorns](https://github.com/NarwhalHorns), [@AntsyLich](https://github.com/AntsyLich)) ([#3308](https://github.com/mihonapp/mihon/pull/3308))
|
||||||
|
- Fixed marking chapters as read deleting their downloads in categories excluded from "Delete after reading" ([@mliem2k](https://github.com/mliem2k)) ([#3558](https://github.com/mihonapp/mihon/pull/3558))
|
||||||
|
- Fixed local source entries missing from global search ([@mugetau](https://github.com/mugetau)) ([#3784](https://github.com/mihonapp/mihon/pull/3784))
|
||||||
|
- Fixed the Updates screen hiding the last update time and pull to refresh when it has no chapters, and saying there are no recent updates when filters hide them all ([@AMendoza180890](https://github.com/AMendoza180890), [@AntsyLich](https://github.com/AntsyLich)) ([#3790](https://github.com/mihonapp/mihon/pull/3790))
|
||||||
|
- Fixed downloads of chapters removed by the source getting stuck in the download queue ([@JoJoKorok](https://github.com/JoJoKorok), [@AntsyLich](https://github.com/AntsyLich)) ([#3845](https://github.com/mihonapp/mihon/pull/3845))
|
||||||
|
- Fixed the stored download index overwriting the startup rescan, which could show downloaded chapters as not downloaded ([@choppeh](https://github.com/choppeh), [@AntsyLich](https://github.com/AntsyLich)) ([#3968](https://github.com/mihonapp/mihon/pull/3968))
|
||||||
|
- Fixed local chapters no longer being listed after one archive couldn't be read ([@a-biad](https://github.com/a-biad), [@AntsyLich](https://github.com/AntsyLich)) ([#3997](https://github.com/mihonapp/mihon/pull/3997))
|
||||||
|
- Fixed "Hide missing chapter indicators" not hiding the indicator in chapter headers ([@kaosjammo](https://github.com/kaosjammo)) ([#4039](https://github.com/mihonapp/mihon/pull/4039))
|
||||||
|
- Fixed a chapter being downloaded a second time when downloads were stopped right as it finished ([@AntsyLich](https://github.com/AntsyLich)) ([`6115783`](https://github.com/mihonapp/mihon/commit/6115783))
|
||||||
|
- Fixed the chapter selection being cleared when new chapters were downloaded automatically after a refresh ([@AntsyLich](https://github.com/AntsyLich)) ([`a82d27c`](https://github.com/mihonapp/mihon/commit/a82d27c))
|
||||||
|
- Fixed actions started from a dialog sometimes not running when the dialog closed right away, Kitsu, Shikimori, MangaBaka, Bangumi and Hikka sometimes refreshing their login twice at once, adding an extension repo served over HTTP failing, and cancelling an extension install not stopping its download ([@AntsyLich](https://github.com/AntsyLich)) ([#4068](https://github.com/mihonapp/mihon/pull/4068))
|
||||||
|
- Fixed the next library update staying "Soon" after new chapters were found, and chapters that came out early, late or were dated earlier by the source pushing the next check back ([@AntsyLich](https://github.com/AntsyLich)) ([`ba44815`](https://github.com/mihonapp/mihon/commit/ba44815))
|
||||||
|
- Fixed long strip side padding being counted in zoom bounds ([@WizaHX](https://github.com/WizaHX)) ([#4072](https://github.com/mihonapp/mihon/pull/4072))
|
||||||
|
- Fixed long strip stuttering between pages and using a lot of memory with very tall pages ([@AntsyLich](https://github.com/AntsyLich), [@matheuswr89](https://github.com/matheuswr89))
|
||||||
|
|
||||||
|
### Other
|
||||||
|
- Show "Nightly" instead of "Beta" for preview builds and "FOSS" for FOSS builds in About ([@AntsyLich](https://github.com/AntsyLich)) ([#3760](https://github.com/mihonapp/mihon/pull/3760))
|
||||||
|
- Migrate Kitsu tracker to Kitsu's GraphQL API ([@MajorTanya](https://github.com/MajorTanya)) ([#3792](https://github.com/mihonapp/mihon/pull/3792))
|
||||||
|
- Fixed Kitsu search showing novel results ([@MajorTanya](https://github.com/MajorTanya)) ([#4069](https://github.com/mihonapp/mihon/pull/4069))
|
||||||
|
- Extensions can only get Application, Context, Json, ProtoBuf, XML, NetworkHelper and JavaScriptEngine from Injekt ([@AntsyLich](https://github.com/AntsyLich)) ([#3965](https://github.com/mihonapp/mihon/pull/3965))
|
||||||
|
- Use Apollo GraphQL for AniList, Kitsu, Shikimori and Suwayomi tracker APIs ([@MajorTanya](https://github.com/MajorTanya), [@Syer10](https://github.com/Syer10)) ([#3827](https://github.com/mihonapp/mihon/pull/3827))
|
||||||
|
|
||||||
|
## [v0.20.4] - 2026-08-05
|
||||||
|
### Fixed
|
||||||
|
- Fixed some dates being wrongly shown as "Today" or otherwise closer in time ([@MajorTanya](https://github.com/MajorTanya)) ([#3696](https://github.com/mihonapp/mihon/pull/3696))
|
||||||
|
|
||||||
|
## [v0.20.3] - 2026-08-04
|
||||||
|
### Added
|
||||||
|
- Add Category filtering for the Updates tab ([@MajorTanya](https://github.com/MajorTanya)) ([#3589](https://github.com/mihonapp/mihon/pull/3589))
|
||||||
|
- Add Category filtering for the Upcoming calendar ([@MajorTanya](https://github.com/MajorTanya)) ([#3607](https://github.com/mihonapp/mihon/pull/3607))
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Update default user agent to Chrome 149 ([@AntsyLich](https://github.com/AntsyLich)) ([#3678](https://github.com/mihonapp/mihon/pull/3678))
|
||||||
|
|
||||||
|
### Improved
|
||||||
|
- Spoof `Sec-CH-UA` client hints in WebView to match the user agent ([@AntsyLich](https://github.com/AntsyLich)) ([#3678](https://github.com/mihonapp/mihon/pull/3678))
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Fixed extension installation with shizuku installer ([@NGB-Was-Taken](https://github.com/NGB-Was-Taken)) ([#3676](https://github.com/mihonapp/mihon/pull/3676))
|
||||||
|
- Fixed `X-Requested-With` spoofing leaking to unrelated callers ([@AntsyLich](https://github.com/AntsyLich)) ([#3678](https://github.com/mihonapp/mihon/pull/3678))
|
||||||
|
- Fixed reader loading indefinitely in some scenarios ([@AntsyLich](https://github.com/AntsyLich)) ([#3686](https://github.com/mihonapp/mihon/pull/3686))
|
||||||
|
- Fix MyAnimeList error for list entries with partial start/finish reading dates by setting those dates to either Jan 1st or 1st of the month ([@MajorTanya](https://github.com/MajorTanya)) ([#3573](https://github.com/mihonapp/mihon/pull/3573))
|
||||||
|
|
||||||
|
## [v0.20.2] - 2026-08-01
|
||||||
|
### Added
|
||||||
|
- Support resumable image downloads if supported by source ([@xMohnad](https://github.com/xMohnad)) ([#3167](https://github.com/mihonapp/mihon/pull/3167))
|
||||||
|
- Fix HTTP Error 416 not being handled properly ([@AntsyLich](https://github.com/AntsyLich)) ([#3563](https://github.com/mihonapp/mihon/pull/3563))
|
||||||
|
- Fix incorrect progress when resuming downloads ([@xMohnad](https://github.com/xMohnad)) ([#3616](https://github.com/mihonapp/mihon/pull/3616))
|
||||||
|
- Add Tokyo Night Theme ([@xMohnad](https://github.com/xMohnad)) ([#3502](https://github.com/mihonapp/mihon/pull/3502))
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Detect Shizuku with permission check ([@Small-Ku](https://github.com/Small-Ku)) ([#3565](https://github.com/mihonapp/mihon/pull/3565))
|
||||||
|
- Check for app and extension update on every cold start ([@AntsyLich](https://github.com/AntsyLich)) ([#3658](https://github.com/mihonapp/mihon/pull/3658))
|
||||||
|
|
||||||
|
### Improved
|
||||||
|
- Library search ([@NGB-Was-Taken](https://github.com/NGB-Was-Taken)) ([#3554](https://github.com/mihonapp/mihon/pull/3554))
|
||||||
|
- Support logical operators (`&&`, `||`, `-`), implicit `AND` separators, and nested expressions with `()`
|
||||||
|
- Add comparison operators (`>`, `>=`, `<`, `<=`, `=`) for `id`, `added`, `fetchinterval` / `fi`, `nextupdate` / `nu`, `unread`, `read`, and `total` (Uses `yyyy-MM-dd` dates for `added` and `nextupdate`)
|
||||||
|
- Support field-specific prefixes (`title`, `author`, `artist`, `description` / `desc`, `genre` / `tag`, `source` / `src`, `source_id` / `src_id`, `notes` / `note`, `language` / `lang`) and empty field matching (`""`, `''`)
|
||||||
|
- Always use Private installer to update privately installed extensions ([@MajorTanya](https://github.com/MajorTanya)) ([#3614](https://github.com/mihonapp/mihon/pull/3614))
|
||||||
|
- App update prompt flow experience ([@AntsyLich](https://github.com/AntsyLich)) ([#3669](https://github.com/mihonapp/mihon/pull/3669))
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Fixed app bars remaining visible after changing pages by tapping in the paged reader after using the chapter navigator slider ([@AntsyLich](https://github.com/AntsyLich)) ([#3567](https://github.com/mihonapp/mihon/pull/3567))
|
||||||
|
- Fixed MangaBaka User Agent string ([@MajorTanya](https://github.com/MajorTanya)) ([#3578](https://github.com/mihonapp/mihon/pull/3578))
|
||||||
|
- Fixed extension installation with shizuku installer ([@NGB-Was-Taken](https://github.com/NGB-Was-Taken)) ([#3630](https://github.com/mihonapp/mihon/pull/3630))
|
||||||
|
- Fixed backup restore dropping library entries when the backup contains duplicate chapters ([@na-ji](https://github.com/na-ji)) ([#3667](https://github.com/mihonapp/mihon/pull/3667))
|
||||||
|
|
||||||
|
## [v0.20.1] - 2026-07-09
|
||||||
|
### Added
|
||||||
|
- [Hikka](https://hikka.io/) tracker support ([@Lorg0n](https://github.com/Lorg0n)) ([#1386](https://github.com/mihonapp/mihon/pull/1386))
|
||||||
|
- Fix Hikka not defaulting to "Plan to Read" for unread titles ([@MajorTanya](https://github.com/MajorTanya)) ([#3534](https://github.com/mihonapp/mihon/pull/3534))
|
||||||
|
- Fix Hikka search throwing error due to unclosed response ([@MajorTanya](https://github.com/MajorTanya)) ([#3548](https://github.com/mihonapp/mihon/pull/3548))
|
||||||
|
- Add support for [MangaBaka](https://mangabaka.org) tracker ([@MajorTanya](https://github.com/MajorTanya)) ([#3047](https://github.com/mihonapp/mihon/pull/3047))
|
||||||
|
- Display authors and description in Shikimori search results ([@MajorTanya](https://github.com/MajorTanya)) ([#3499](https://github.com/mihonapp/mihon/pull/3499))
|
||||||
|
- Invalidate download cache after backup restore ([@leodyversemilla07](https://github.com/leodyversemilla07)) ([#3096](https://github.com/mihonapp/mihon/pull/3096))
|
||||||
|
- Add setting to control vertical chapter navigator height ([@AntsyLich](https://github.com/AntsyLich)) ([#3528](https://github.com/mihonapp/mihon/pull/3528))
|
||||||
|
- Add option to set which readers use vertical chapter navigator ([@AntsyLich](https://github.com/AntsyLich)) ([#3528](https://github.com/mihonapp/mihon/pull/3528))
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Don't use GMS to detect if GMS is available ([@leodyversemilla07](https://github.com/leodyversemilla07)) ([#3525](https://github.com/mihonapp/mihon/pull/3525))
|
||||||
|
|
||||||
|
### Improved
|
||||||
|
- Show usernames in Tracking settings ([@MajorTanya](https://github.com/MajorTanya)) ([#3533](https://github.com/mihonapp/mihon/pull/3533))
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Fix Shikimori tracking not working ([@MajorTanya](https://github.com/MajorTanya)) ([#3497](https://github.com/mihonapp/mihon/pull/3497))
|
||||||
|
- Fix crash trying to select text in notes screen ([@AntsyLich](https://github.com/AntsyLich)) ([#3516](https://github.com/mihonapp/mihon/pull/3516))
|
||||||
|
- Fix crash when putting app in background ([@AntsyLich](https://github.com/AntsyLich)) ([#3523](https://github.com/mihonapp/mihon/pull/3523))
|
||||||
|
- Fix support for non-system SAF providers ([@AntsyLich](https://github.com/AntsyLich)) ([#3530](https://github.com/mihonapp/mihon/pull/3530))
|
||||||
|
- Fix chapter's `memo` field not being updated for existing chapters ([@cuong-tran](https://github.com/cuong-tran)) ([#3538](https://github.com/mihonapp/mihon/pull/3538))
|
||||||
|
- Fix app not reading `tachiyomix.extensionLib` extension metadata ([@AntsyLich](https://github.com/AntsyLich)) ([#3545](https://github.com/mihonapp/mihon/pull/3545), [#3559](https://github.com/mihonapp/mihon/pull/3559))
|
||||||
|
- Fix reader navigator slider steps not updating after changing chapter ([@AntsyLich](https://github.com/AntsyLich)) ([#3549](https://github.com/mihonapp/mihon/pull/3549))
|
||||||
|
|
||||||
|
## [v0.20.0] - 2026-06-27
|
||||||
|
### Added
|
||||||
|
- Add support for `tachiyomix` 1.6 extensions ([@AntsyLich](https://github.com/AntsyLich))
|
||||||
|
- Add support for `tachiyomix` extension index format ([@AntsyLich](https://github.com/AntsyLich)) ([#3349](https://github.com/mihonapp/mihon/pull/3349))
|
||||||
|
- Add vertical chapter navigator for long strip mode ([@AntsyLich](https://github.com/AntsyLich)) ([#3377](https://github.com/mihonapp/mihon/pull/3377))
|
||||||
|
- Add option to open app settings from system settings ([@JFronny](https://github.com/JFronny)) ([#3180](https://github.com/mihonapp/mihon/pull/3180))
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Change the term "extension repo" to "extension store" ([@AntsyLich](https://github.com/AntsyLich)) ([#3349](https://github.com/mihonapp/mihon/pull/3349))
|
||||||
|
- Change the term "Obsolete" to "Orphaned" for extensions ([@AntsyLich](https://github.com/AntsyLich)) ([#3383](https://github.com/mihonapp/mihon/pull/3383))
|
||||||
|
- Remove text limit of manga notes ([@AntsyLich](https://github.com/AntsyLich)) ([#3410](https://github.com/mihonapp/mihon/pull/3410))
|
||||||
|
- Tweak Catppuccin color scheme for clarity ([@xenitane](https://github.com/xenitane)) ([#3436](https://github.com/mihonapp/mihon/pull/3436))
|
||||||
|
- Update app shortcuts to use app icon color scheme ([@NGB-Was-Taken](https://github.com/NGB-Was-Taken)) ([#2835](https://github.com/mihonapp/mihon/pull/2835))
|
||||||
|
|
||||||
|
### Improved
|
||||||
|
- Batch database operations during backup restore for improved performance ([@Lolle2000la](https://github.com/Lolle2000la)) ([#3267](https://github.com/mihonapp/mihon/pull/3267))
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Add missing `outlineVariant` color to Nord theme ([@CompileConnected](https://github.com/CompileConnected)) ([#3184](https://github.com/mihonapp/mihon/pull/3184))
|
||||||
|
- Continue reading button missing when unread filter is off ([@AntsyLich](https://github.com/AntsyLich)) ([#3382](https://github.com/mihonapp/mihon/pull/3382))
|
||||||
|
- Fix random `FileNotFoundException` when reading chapters ([@leodyversemilla07](https://github.com/leodyversemilla07)) ([#3154](https://github.com/mihonapp/mihon/pull/3154))
|
||||||
|
|
||||||
|
## [v0.19.9] - 2026-04-11
|
||||||
|
### Fixed
|
||||||
|
- Regression with installing/updating extension ([@AntsyLich](https://github.com/AntsyLich))
|
||||||
|
|
||||||
|
## [v0.19.8] - 2026-04-11
|
||||||
|
### Added
|
||||||
|
- Add dedicated "Support Us" screen ([@AntsyLich](https://github.com/AntsyLich)) ([#3200](https://github.com/mihonapp/mihon/pull/3200))
|
||||||
|
- Add a one time popup asking for donation from long time users ([@AntsyLich](https://github.com/AntsyLich)) ([#3203](https://github.com/mihonapp/mihon/pull/3203))
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Don't prompt users to enable Google Play services if disabled or unavailable ([@leodyversemilla07](https://github.com/leodyversemilla07)) ([#3152](https://github.com/mihonapp/mihon/pull/3152))
|
||||||
|
- Make "Support Us" more prominent in More screen ([@AntsyLich](https://github.com/AntsyLich)) ([#3196](https://github.com/mihonapp/mihon/pull/3196))
|
||||||
|
|
||||||
|
### Improved
|
||||||
|
- Show informative error when trying to add unapproved titles to list on MAL ([@MajorTanya](https://github.com/MajorTanya)) ([#3155](https://github.com/mihonapp/mihon/pull/3155))
|
||||||
|
- Show AniList publishing type based on country of origin ([@zweimach](https://github.com/zweimach)) ([#3100](https://github.com/mihonapp/mihon/pull/3100))
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Fix app trying to split long strip when not needed ([@leodyversemilla07](https://github.com/leodyversemilla07)) ([#3121](https://github.com/mihonapp/mihon/pull/3121))
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
- Remove the empty "Updated to X.Y.Z" popup after app update ([@AntsyLich](https://github.com/AntsyLich)) ([#3203](https://github.com/mihonapp/mihon/pull/3203))
|
||||||
|
|
||||||
|
## [v0.19.7] - 2026-03-23
|
||||||
|
Same as v0.19.6
|
||||||
|
|
||||||
|
## [v0.19.6] - 2026-03-23
|
||||||
|
### Fixed
|
||||||
|
- Fix app crashing when trying to add extension repo with existing signature ([@AntsyLich](https://github.com/AntsyLich)) ([`cb0e329`](https://github.com/mihonapp/mihon/commit/cb0e329))
|
||||||
|
- Potentially fix 'database is locked' crash ([@AntsyLich](https://github.com/AntsyLich)) ([`f8e82b9`](https://github.com/mihonapp/mihon/commit/f8e82b9))
|
||||||
|
- Fix occasional crash when mass installing/uninstalling extension using `PackageManager` ([@AntsyLich](https://github.com/AntsyLich)) ([`42daa3f`](https://github.com/mihonapp/mihon/commit/42daa3f))
|
||||||
|
- Fix app crash on startup on some Android TV ([@AntsyLich](https://github.com/AntsyLich)) ([`b40f1d3`](https://github.com/mihonapp/mihon/commit/b40f1d3))
|
||||||
|
|
||||||
|
## [v0.19.5] - 2026-03-20
|
||||||
|
### Changed
|
||||||
|
- Retry in reader now redownloads image ([@AntsyLich](https://github.com/AntsyLich)) ([#3089](https://github.com/mihonapp/mihon/pull/3089))
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Fix performance regression introduced in v0.19.4 ([@AntsyLich](https://github.com/AntsyLich)) ([#3082](https://github.com/mihonapp/mihon/pull/3082))
|
||||||
|
- Fix duplicate key crash in duplicate detection ([@leodyversemilla07](https://github.com/leodyversemilla07)) ([#3040](https://github.com/mihonapp/mihon/pull/3040))
|
||||||
|
- Fix MangaUpdates HTTP 4XX errors ([@leodyversemilla07](https://github.com/leodyversemilla07)) ([#3021](https://github.com/mihonapp/mihon/pull/3021))
|
||||||
|
- Fix WebView JavaScript dialogs popup after screen is closed ([@leodyversemilla07](https://github.com/leodyversemilla07)) ([#3041](https://github.com/mihonapp/mihon/pull/3041))
|
||||||
|
- Fix extension actions disappearing after installing and uninstalling in same session ([@leodyversemilla07](https://github.com/leodyversemilla07)) ([#3049](https://github.com/mihonapp/mihon/pull/3049))
|
||||||
|
|
||||||
|
## [v0.19.4] - 2026-02-25
|
||||||
|
### Added
|
||||||
|
- Automatically remove downloads on Suwayomi after reading, configurable via extension settings ([@cpiber](https://github.com/cpiber)) ([#2673](https://github.com/mihonapp/mihon/pull/2673))
|
||||||
|
- Display author & artist name in MAL search results ([@MajorTanya](https://github.com/MajorTanya)) ([#2833](https://github.com/mihonapp/mihon/pull/2833))
|
||||||
|
- Add filter options to Updates tab ([@MajorTanya](https://github.com/MajorTanya)) ([#2851](https://github.com/mihonapp/mihon/pull/2851))
|
||||||
|
- Add bookmarked chapters to chapter download options ([@NarwhalHorns](https://github.com/NarwhalHorns)) ([#2891](https://github.com/mihonapp/mihon/pull/2891))
|
||||||
|
- Add `src:` prefix to search the library by source ID ([@MajorTanya](https://github.com/MajorTanya)) ([#2927](https://github.com/mihonapp/mihon/pull/2927))
|
||||||
|
- Add `src:local` as a way to search for Local Source entries ([@MajorTanya](https://github.com/MajorTanya)) ([#2928](https://github.com/mihonapp/mihon/pull/2928))
|
||||||
|
|
||||||
|
### Improved
|
||||||
|
- Minimize memory usage by reducing in-memory cover cache size ([@Lolle2000la](https://github.com/Lolle2000la)) ([#2266](https://github.com/mihonapp/mihon/pull/2266))
|
||||||
|
- Optimize MAL search queries ([@MajorTanya](https://github.com/MajorTanya)) ([#2832](https://github.com/mihonapp/mihon/pull/2832))
|
||||||
|
- Reword download reindexing message to avoid confusion ([@MajorTanya](https://github.com/MajorTanya)) ([#2874](https://github.com/mihonapp/mihon/pull/2874))
|
||||||
|
- Optimize tracked library filter ([@NarwhalHorns](https://github.com/NarwhalHorns)) ([#2977](https://github.com/mihonapp/mihon/pull/2977))
|
||||||
|
- Utilize tracker for library duplicate detection ([@NarwhalHorns](https://github.com/NarwhalHorns)) ([#2978](https://github.com/mihonapp/mihon/pull/2978))
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Update tracker icons ([@AntsyLich](https://github.com/AntsyLich)) ([#2773](https://github.com/mihonapp/mihon/pull/2773))
|
||||||
|
- Add a small increment to chapter number before comparison to fix progress sync issues for Suwayomi ([@cpiber](https://github.com/cpiber)) ([#2657](https://github.com/mihonapp/mihon/pull/2675))
|
||||||
|
- Add all pages of adjacent chapters in the UI instead of only the first or last three ([@AntsyLich](https://github.com/AntsyLich)) ([#2995](https://github.com/mihonapp/mihon/pull/2995))
|
||||||
|
- Going back now first clears search query on browse extension tab ([@cuong-tran](https://github.com/cuong-tran)) ([#2906](https://github.com/mihonapp/mihon/pull/2906))
|
||||||
|
- Automatic library updates now run even when connected to a VPN ([@AntsyLich](https://github.com/AntsyLich)) ([#2773](https://github.com/mihonapp/mihon/pull/2773))
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Fix reader tap zones triggering after scrolling is stopped by tapping ([@NGB-Was-Taken](https://github.com/NGB-Was-Taken)) ([#2680](https://github.com/mihonapp/mihon/pull/2680))
|
||||||
|
- Fix shizuku installer not updating installed extensions ([@NGB-Was-Taken](https://github.com/NGB-Was-Taken)) ([#2697](https://github.com/mihonapp/mihon/pull/2697))
|
||||||
|
- Fix mass migration not using the same search queries as individual migration ([@AntsyLich](https://github.com/AntsyLich)) ([#2736](https://github.com/mihonapp/mihon/pull/2736))
|
||||||
|
- Fix reader not saving read duration when changing chapter ([@AntsyLich](https://github.com/AntsyLich), [@KotlinHero](https://github.com/KotlinHero)) ([#2784](https://github.com/mihonapp/mihon/pull/2784))
|
||||||
|
- Fix pre-1970 upload date display in chapter list ([@MajorTanya](https://github.com/MajorTanya)) ([#2779](https://github.com/mihonapp/mihon/pull/2779))
|
||||||
|
- Fix crash when trying to install/update extensions while shizuku is not running ([@NGB-Was-Taken](https://github.com/NGB-Was-Taken)) ([#2837](https://github.com/mihonapp/mihon/pull/2837))
|
||||||
|
- Fix Add Repo input not taking up the full dialog width ([@cuong-tran](https://github.com/cuong-tran)) ([#2816](https://github.com/mihonapp/mihon/pull/2816))
|
||||||
|
- Fix migration's selected sources order not preserved ([@AntsyLich](https://github.com/AntsyLich)) ([#2993](https://github.com/mihonapp/mihon/pull/2993))
|
||||||
|
- Fix migration dialog not showing for consecutive prompts from the same screen ([@AntsyLich](https://github.com/AntsyLich)) ([#2994](https://github.com/mihonapp/mihon/pull/2994))
|
||||||
|
- Fix extension install/update stuck at pending ([@AntsyLich](https://github.com/AntsyLich)) ([#3000](https://github.com/mihonapp/mihon/pull/3000))
|
||||||
|
- Fix crash when downloading multiple chapters with Arabic locale ([@MajorTanya](https://github.com/MajorTanya)) ([#2676](https://github.com/mihonapp/mihon/pull/2676))
|
||||||
|
|
||||||
|
### Other
|
||||||
|
- Enable logcat logging on stable and debug builds without enabling verbose logging ([@NGB-Was-Taken](https://github.com/NGB-Was-Taken)) ([#2836](https://github.com/mihonapp/mihon/pull/2836))
|
||||||
|
|
||||||
|
## [v0.19.3] - 2025-11-07
|
||||||
|
### Improved
|
||||||
|
- Improved various aspects of the WebView multi window support ([@TheUnlocked](https://github.com/TheUnlocked)) ([#2662](https://github.com/mihonapp/mihon/pull/2662))
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
- Revert "Fix reader tap zones triggering after scrolling was stopped by the user" due to introduction of regression ([@AntsyLich](https://github.com/AntsyLich)) ([#2670](https://github.com/mihonapp/mihon/pull/2670))
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Fix WebView crash introduced in 0.19.2 ([@bapeey](https://github.com/bapeey)) ([#2649](https://github.com/mihonapp/mihon/pull/2649))
|
||||||
|
- Fix extra padding appearing in reader after user interactions ([@AntsyLich](https://github.com/AntsyLich)) ([#2669](https://github.com/mihonapp/mihon/pull/2669))
|
||||||
|
- Fix long strip reader not scrolling on consecutive taps ([@AntsyLich](https://github.com/AntsyLich)) ([#2670](https://github.com/mihonapp/mihon/pull/2670))
|
||||||
|
|
||||||
## [v0.19.2] - 2025-11-02
|
## [v0.19.2] - 2025-11-02
|
||||||
### Added
|
### Added
|
||||||
- Advanced setting to limit download filenames to ASCII characters. This is provided only as a workaround for OSes that do not properly handle standard Unicode filenames. This setting is generally not recommended and should only be used as a last resort ([@raxod502](https://github.com/radian-software)) ([#2305](https://github.com/mihonapp/mihon/pull/2305))
|
- Advanced setting to limit download filenames to ASCII characters. This is provided only as a workaround for OSes that do not properly handle standard Unicode filenames. This setting is generally not recommended and should only be used as a last resort ([@raxod502](https://github.com/radian-software)) ([#2305](https://github.com/mihonapp/mihon/pull/2305))
|
||||||
@@ -321,7 +674,7 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co
|
|||||||
- Fix MigratorTest after updating to Kotlin 2 ([@cuong-tran](https://github.com/cuong-tran)) ([#896](https://github.com/mihonapp/mihon/pull/896))
|
- Fix MigratorTest after updating to Kotlin 2 ([@cuong-tran](https://github.com/cuong-tran)) ([#896](https://github.com/mihonapp/mihon/pull/896))
|
||||||
- Add MigratorTest to build script ([@cuong-tran](https://github.com/cuong-tran)) ([#896](https://github.com/mihonapp/mihon/pull/896))
|
- Add MigratorTest to build script ([@cuong-tran](https://github.com/cuong-tran)) ([#896](https://github.com/mihonapp/mihon/pull/896))
|
||||||
- Fix UI freeze after migration ([@AntsyLich](https://github.com/AntsyLich)) ([`3f1d28c`](https://github.com/mihonapp/mihon/commit/3f1d28c3833e6b868152149ed02b3fb8c54eccef))
|
- Fix UI freeze after migration ([@AntsyLich](https://github.com/AntsyLich)) ([`3f1d28c`](https://github.com/mihonapp/mihon/commit/3f1d28c3833e6b868152149ed02b3fb8c54eccef))
|
||||||
- Fix some migrations never running ([@MajorTanya](https://github.com/MajorTanya), [@AntsyLich](https://github.com/AntsyLich)) ([#1030](https://github.com/mihonapp/mihon/pull/1030))
|
- Fix some migrations never running ([@MajorTanya](https://github.com/MajorTanya), [@AntsyLich](https://github.com/AntsyLich)) ([#1114](https://github.com/mihonapp/mihon/pull/1114))
|
||||||
- Add ProGuard rule to keep `mihon` namespace classes ([@MajorTanya](https://github.com/MajorTanya)) ([#605](https://github.com/mihonapp/mihon/pull/605))
|
- Add ProGuard rule to keep `mihon` namespace classes ([@MajorTanya](https://github.com/MajorTanya)) ([#605](https://github.com/mihonapp/mihon/pull/605))
|
||||||
- Use gradle plugins to share build configuration instead of subprojects ([@AntsyLich](https://github.com/AntsyLich)) ([`e448e40`](https://github.com/mihonapp/mihon/commit/e448e40406e8d9916120a278e42829a6f1b25a7a))
|
- Use gradle plugins to share build configuration instead of subprojects ([@AntsyLich](https://github.com/AntsyLich)) ([`e448e40`](https://github.com/mihonapp/mihon/commit/e448e40406e8d9916120a278e42829a6f1b25a7a))
|
||||||
- Remove dependency on compose material 2 components ([@AntsyLich](https://github.com/AntsyLich)) ([`fb94230`](https://github.com/mihonapp/mihon/commit/fb9423028eb017c110cb805f2d0601e5b02e50f9))
|
- Remove dependency on compose material 2 components ([@AntsyLich](https://github.com/AntsyLich)) ([`fb94230`](https://github.com/mihonapp/mihon/commit/fb9423028eb017c110cb805f2d0601e5b02e50f9))
|
||||||
@@ -433,7 +786,22 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co
|
|||||||
- Branding to Mihon ([@AntsyLich](https://github.com/AntsyLich))
|
- Branding to Mihon ([@AntsyLich](https://github.com/AntsyLich))
|
||||||
- Minimum supported Android version to 8 ([@AntsyLich](https://github.com/AntsyLich)) ([`dfb3091`](https://github.com/mihonapp/mihon/commit/dfb3091e380dda3e9bfb64bf5c9a685cf3a03d0e))
|
- Minimum supported Android version to 8 ([@AntsyLich](https://github.com/AntsyLich)) ([`dfb3091`](https://github.com/mihonapp/mihon/commit/dfb3091e380dda3e9bfb64bf5c9a685cf3a03d0e))
|
||||||
|
|
||||||
[unreleased]: https://github.com/mihonapp/mihon/compare/v0.19.2...main
|
[unreleased]: https://github.com/mihonapp/mihon/compare/v0.21.2...main
|
||||||
|
[v0.21.2]: https://github.com/mihonapp/mihon/compare/v0.21.1...v0.21.2
|
||||||
|
[v0.21.1]: https://github.com/mihonapp/mihon/compare/v0.21.0...v0.21.1
|
||||||
|
[v0.21.0]: https://github.com/mihonapp/mihon/compare/v0.20.4...v0.21.0
|
||||||
|
[v0.20.4]: https://github.com/mihonapp/mihon/compare/v0.20.3...v0.20.4
|
||||||
|
[v0.20.3]: https://github.com/mihonapp/mihon/compare/v0.20.2...v0.20.3
|
||||||
|
[v0.20.2]: https://github.com/mihonapp/mihon/compare/v0.20.1...v0.20.2
|
||||||
|
[v0.20.1]: https://github.com/mihonapp/mihon/compare/v0.20.0...v0.20.1
|
||||||
|
[v0.20.0]: https://github.com/mihonapp/mihon/compare/v0.19.9...v0.20.0
|
||||||
|
[v0.19.9]: https://github.com/mihonapp/mihon/compare/v0.19.8...v0.19.9
|
||||||
|
[v0.19.8]: https://github.com/mihonapp/mihon/compare/v0.19.7...v0.19.8
|
||||||
|
[v0.19.7]: https://github.com/mihonapp/mihon/compare/v0.19.6...v0.19.7
|
||||||
|
[v0.19.6]: https://github.com/mihonapp/mihon/compare/v0.19.5...v0.19.6
|
||||||
|
[v0.19.5]: https://github.com/mihonapp/mihon/compare/v0.19.4...v0.19.5
|
||||||
|
[v0.19.4]: https://github.com/mihonapp/mihon/compare/v0.19.3...v0.19.4
|
||||||
|
[v0.19.3]: https://github.com/mihonapp/mihon/compare/v0.19.2...v0.19.3
|
||||||
[v0.19.2]: https://github.com/mihonapp/mihon/compare/v0.19.1...v0.19.2
|
[v0.19.2]: https://github.com/mihonapp/mihon/compare/v0.19.1...v0.19.2
|
||||||
[v0.19.1]: https://github.com/mihonapp/mihon/compare/v0.19.0...v0.19.1
|
[v0.19.1]: https://github.com/mihonapp/mihon/compare/v0.19.0...v0.19.1
|
||||||
[v0.19.0]: https://github.com/mihonapp/mihon/compare/v0.18.0...v0.19.0
|
[v0.19.0]: https://github.com/mihonapp/mihon/compare/v0.18.0...v0.19.0
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ Discover and read manga, webtoons, comics, and more – easier than ever on your
|
|||||||
|
|
||||||
* Local reading of content.
|
* Local reading of content.
|
||||||
* A configurable reader with multiple viewers, reading directions and other settings.
|
* A configurable reader with multiple viewers, reading directions and other settings.
|
||||||
* Tracker support: [MyAnimeList](https://myanimelist.net/), [AniList](https://anilist.co/), [Kitsu](https://kitsu.app/), [MangaUpdates](https://mangaupdates.com), [Shikimori](https://shikimori.one), and [Bangumi](https://bgm.tv/) support.
|
* Tracker support: [MangaBaka](https://mangabaka.org), [MyAnimeList](https://myanimelist.net/), [AniList](https://anilist.co/), [Kitsu](https://kitsu.app/), [MangaUpdates](https://mangaupdates.com), [Shikimori](https://shikimori.one), [Bangumi](https://bgm.tv/), and [Hikka](https://hikka.io/) support.
|
||||||
* Categories to organize your library.
|
* Categories to organize your library.
|
||||||
* Light and dark themes.
|
* Light and dark themes.
|
||||||
* Schedule updating your library for new chapters.
|
* Schedule updating your library for new chapters.
|
||||||
@@ -49,8 +49,8 @@ Before reporting a new issue, take a look at the [FAQ](https://mihon.app/docs/fa
|
|||||||
|
|
||||||
### Repositories
|
### Repositories
|
||||||
|
|
||||||
[](https://github.com/mihonapp/website/)
|
[](https://github.com/mihonapp/website/)
|
||||||
[](https://github.com/mihonapp/bitmap.kt/)
|
[](https://github.com/mihonapp/bitmap.kt/)
|
||||||
|
|
||||||
### Credits
|
### Credits
|
||||||
|
|
||||||
|
|||||||
+233
-88
@@ -1,14 +1,25 @@
|
|||||||
import mihon.buildlogic.Config
|
import com.android.build.api.variant.ApplicationVariant
|
||||||
import mihon.buildlogic.getBuildTime
|
import com.android.build.api.variant.BuildConfigField
|
||||||
import mihon.buildlogic.getCommitCount
|
import com.google.firebase.crashlytics.buildtools.gradle.CrashlyticsExtension
|
||||||
import mihon.buildlogic.getGitSha
|
import mihon.gradle.Config
|
||||||
|
import mihon.gradle.getCurrentTime
|
||||||
|
import mihon.gradle.getLatestCommitCount
|
||||||
|
import mihon.gradle.getLatestCommitSha
|
||||||
|
import mihon.gradle.getLatestCommitTime
|
||||||
|
import mihon.gradle.tasks.ReplaceShortcutsPlaceholderTask
|
||||||
|
import java.io.FileInputStream
|
||||||
|
import java.util.Properties
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("mihon.android.application")
|
alias(mihonx.plugins.android.application)
|
||||||
id("mihon.android.application.compose")
|
alias(mihonx.plugins.compose)
|
||||||
id("com.github.zellius.shortcut-helper")
|
alias(mihonx.plugins.spotless)
|
||||||
kotlin("plugin.serialization")
|
|
||||||
|
alias(libs.plugins.metro)
|
||||||
alias(libs.plugins.aboutLibraries)
|
alias(libs.plugins.aboutLibraries)
|
||||||
|
alias(libs.plugins.androidx.baselineProfile)
|
||||||
|
alias(libs.plugins.kotlin.serialization)
|
||||||
|
alias(libs.plugins.apollo)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Config.includeTelemetry) {
|
if (Config.includeTelemetry) {
|
||||||
@@ -18,7 +29,7 @@ if (Config.includeTelemetry) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
shortcutHelper.setFilePath("./shortcuts.xml")
|
val keystorePropertiesFile = layout.settingsDirectory.file("keystore.properties").asFile
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "eu.kanade.tachiyomi"
|
namespace = "eu.kanade.tachiyomi"
|
||||||
@@ -26,31 +37,51 @@ android {
|
|||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "app.mihon"
|
applicationId = "app.mihon"
|
||||||
|
|
||||||
versionCode = 15
|
versionCode = 38
|
||||||
versionName = "0.19.2"
|
versionName = "0.21.2"
|
||||||
|
|
||||||
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
|
||||||
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
|
|
||||||
buildConfigField("String", "BUILD_TIME", "\"${getBuildTime(useLastCommitTime = false)}\"")
|
|
||||||
buildConfigField("boolean", "TELEMETRY_INCLUDED", "${Config.includeTelemetry}")
|
buildConfigField("boolean", "TELEMETRY_INCLUDED", "${Config.includeTelemetry}")
|
||||||
buildConfigField("boolean", "UPDATER_ENABLED", "${Config.enableUpdater}")
|
buildConfigField("boolean", "UPDATER_ENABLED", "${Config.enableUpdater}")
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (System.getenv("MIHON_GITHUB_RELEASE").toBoolean()) {
|
||||||
|
signingConfigs {
|
||||||
|
named("debug") {
|
||||||
|
storeFile = file(System.getenv("storeFile"))
|
||||||
|
storePassword = System.getenv("storePassword")
|
||||||
|
keyAlias = System.getenv("keyAlias")
|
||||||
|
keyPassword = System.getenv("keyPassword")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (keystorePropertiesFile.exists()) {
|
||||||
|
val keystoreProperties = FileInputStream(keystorePropertiesFile).use { Properties().apply { load(it) } }
|
||||||
|
|
||||||
|
signingConfigs {
|
||||||
|
named("debug") {
|
||||||
|
storeFile = file(keystoreProperties.getProperty("storeFile"))
|
||||||
|
storePassword = keystoreProperties.getProperty("storePassword")
|
||||||
|
keyAlias = keystoreProperties.getProperty("keyAlias")
|
||||||
|
keyPassword = keystoreProperties.getProperty("keyPassword")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
val debug by getting {
|
val debug = getByName("debug") {
|
||||||
applicationIdSuffix = ".dev"
|
applicationIdSuffix = ".dev"
|
||||||
versionNameSuffix = "-${getCommitCount()}"
|
|
||||||
isPseudoLocalesEnabled = true
|
isPseudoLocalesEnabled = true
|
||||||
}
|
}
|
||||||
val release by getting {
|
val release = getByName("release") {
|
||||||
isMinifyEnabled = Config.enableCodeShrink
|
isMinifyEnabled = true
|
||||||
isShrinkResources = Config.enableCodeShrink
|
isShrinkResources = true
|
||||||
|
|
||||||
|
signingConfig = debug.signingConfig
|
||||||
|
|
||||||
|
isProfileable = true
|
||||||
|
|
||||||
proguardFiles("proguard-android-optimize.txt", "proguard-rules.pro")
|
proguardFiles("proguard-android-optimize.txt", "proguard-rules.pro")
|
||||||
|
|
||||||
buildConfigField("String", "BUILD_TIME", "\"${getBuildTime(useLastCommitTime = true)}\"")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val commonMatchingFallbacks = listOf(release.name)
|
val commonMatchingFallbacks = listOf(release.name)
|
||||||
@@ -62,35 +93,34 @@ android {
|
|||||||
|
|
||||||
matchingFallbacks.addAll(commonMatchingFallbacks)
|
matchingFallbacks.addAll(commonMatchingFallbacks)
|
||||||
}
|
}
|
||||||
create("preview") {
|
create("nightly") {
|
||||||
initWith(release)
|
initWith(release)
|
||||||
|
|
||||||
applicationIdSuffix = ".debug"
|
applicationIdSuffix = ".debug"
|
||||||
|
|
||||||
versionNameSuffix = debug.versionNameSuffix
|
|
||||||
signingConfig = debug.signingConfig
|
|
||||||
|
|
||||||
matchingFallbacks.addAll(commonMatchingFallbacks)
|
matchingFallbacks.addAll(commonMatchingFallbacks)
|
||||||
|
|
||||||
buildConfigField("String", "BUILD_TIME", "\"${getBuildTime(useLastCommitTime = false)}\"")
|
|
||||||
}
|
}
|
||||||
create("benchmark") {
|
create("benchmark") {
|
||||||
initWith(release)
|
initWith(release)
|
||||||
|
|
||||||
isDebuggable = false
|
|
||||||
isProfileable = true
|
|
||||||
versionNameSuffix = "-benchmark"
|
versionNameSuffix = "-benchmark"
|
||||||
applicationIdSuffix = ".benchmark"
|
applicationIdSuffix = ".benchmark"
|
||||||
|
|
||||||
signingConfig = debug.signingConfig
|
|
||||||
|
|
||||||
matchingFallbacks.addAll(commonMatchingFallbacks)
|
matchingFallbacks.addAll(commonMatchingFallbacks)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Config.includeTelemetry) {
|
||||||
|
configureEach {
|
||||||
|
configure<CrashlyticsExtension> {
|
||||||
|
mappingFileUploadEnabled = Config.uploadCrashlyticsMapping
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
getByName("preview").res.srcDirs("src/debug/res")
|
getByName("nightly").res.directories.add("src/debug/res")
|
||||||
getByName("benchmark").res.srcDirs("src/debug/res")
|
getByName("benchmark").res.directories.add("src/debug/res")
|
||||||
}
|
}
|
||||||
|
|
||||||
splits {
|
splits {
|
||||||
@@ -139,10 +169,6 @@ android {
|
|||||||
viewBinding = true
|
viewBinding = true
|
||||||
buildConfig = true
|
buildConfig = true
|
||||||
aidl = true
|
aidl = true
|
||||||
|
|
||||||
// Disable some unused things
|
|
||||||
renderScript = false
|
|
||||||
shaders = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
lint {
|
lint {
|
||||||
@@ -159,6 +185,7 @@ kotlin {
|
|||||||
"-opt-in=androidx.compose.foundation.ExperimentalFoundationApi",
|
"-opt-in=androidx.compose.foundation.ExperimentalFoundationApi",
|
||||||
"-opt-in=androidx.compose.foundation.layout.ExperimentalLayoutApi",
|
"-opt-in=androidx.compose.foundation.layout.ExperimentalLayoutApi",
|
||||||
"-opt-in=androidx.compose.material3.ExperimentalMaterial3Api",
|
"-opt-in=androidx.compose.material3.ExperimentalMaterial3Api",
|
||||||
|
"-opt-in=androidx.compose.material3.ExperimentalMaterial3ExpressiveApi",
|
||||||
"-opt-in=androidx.compose.ui.ExperimentalComposeUiApi",
|
"-opt-in=androidx.compose.ui.ExperimentalComposeUiApi",
|
||||||
"-opt-in=coil3.annotation.ExperimentalCoilApi",
|
"-opt-in=coil3.annotation.ExperimentalCoilApi",
|
||||||
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
||||||
@@ -169,10 +196,21 @@ kotlin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
baselineProfile {
|
||||||
|
baselineProfileOutputDir = "baselineProfiles"
|
||||||
|
mergeIntoMain = true
|
||||||
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
baselineProfile(projects.baselineProfile)
|
||||||
|
|
||||||
implementation(projects.i18n)
|
implementation(projects.i18n)
|
||||||
|
implementation(projects.icons.customMaterialSymbols)
|
||||||
|
implementation(projects.icons.materialSymbols)
|
||||||
|
implementation(projects.icons.simpleIcons)
|
||||||
implementation(projects.core.archive)
|
implementation(projects.core.archive)
|
||||||
implementation(projects.core.common)
|
implementation(projects.core.common)
|
||||||
|
implementation(projects.core.metro)
|
||||||
implementation(projects.coreMetadata)
|
implementation(projects.coreMetadata)
|
||||||
implementation(projects.sourceApi)
|
implementation(projects.sourceApi)
|
||||||
implementation(projects.sourceLocal)
|
implementation(projects.sourceLocal)
|
||||||
@@ -183,93 +221,94 @@ dependencies {
|
|||||||
implementation(projects.telemetry)
|
implementation(projects.telemetry)
|
||||||
|
|
||||||
// Compose
|
// Compose
|
||||||
implementation(compose.activity)
|
implementation(libs.androidx.activity.compose)
|
||||||
implementation(compose.foundation)
|
implementation(libs.androidx.compose.foundation)
|
||||||
implementation(compose.material3.core)
|
implementation(libs.androidx.compose.material3)
|
||||||
implementation(compose.material.icons)
|
implementation(libs.androidx.compose.animation)
|
||||||
implementation(compose.animation)
|
implementation(libs.androidx.compose.animationGraphics)
|
||||||
implementation(compose.animation.graphics)
|
debugImplementation(libs.androidx.compose.uiTooling)
|
||||||
debugImplementation(compose.ui.tooling)
|
implementation(libs.androidx.compose.uiToolingPreview)
|
||||||
implementation(compose.ui.tooling.preview)
|
implementation(libs.androidx.compose.uiUtil)
|
||||||
implementation(compose.ui.util)
|
implementation(libs.androidx.navigationEvent)
|
||||||
|
|
||||||
implementation(androidx.interpolator)
|
implementation(libs.androidx.interpolator)
|
||||||
|
|
||||||
implementation(androidx.paging.runtime)
|
implementation(libs.androidx.paging.runtime)
|
||||||
implementation(androidx.paging.compose)
|
implementation(libs.androidx.paging.compose)
|
||||||
|
|
||||||
implementation(libs.bundles.sqlite)
|
implementation(libs.kotlin.reflect)
|
||||||
|
|
||||||
implementation(kotlinx.reflect)
|
implementation(libs.bundles.kotlinx.coroutines)
|
||||||
implementation(kotlinx.immutables)
|
|
||||||
|
|
||||||
implementation(platform(kotlinx.coroutines.bom))
|
implementation(libs.kotlinx.datetime)
|
||||||
implementation(kotlinx.bundles.coroutines)
|
|
||||||
|
|
||||||
// AndroidX libraries
|
// AndroidX libraries
|
||||||
implementation(androidx.annotation)
|
implementation(libs.androidx.annotation)
|
||||||
implementation(androidx.appcompat)
|
implementation(libs.androidx.appCompat)
|
||||||
implementation(androidx.biometricktx)
|
implementation(libs.androidx.biometric)
|
||||||
implementation(androidx.constraintlayout)
|
implementation(libs.androidx.constraintLayout)
|
||||||
implementation(androidx.corektx)
|
implementation(libs.androidx.core)
|
||||||
implementation(androidx.splashscreen)
|
implementation(libs.androidx.coreSplashScreen)
|
||||||
implementation(androidx.recyclerview)
|
implementation(libs.androidx.recyclerView)
|
||||||
implementation(androidx.viewpager)
|
implementation(libs.androidx.viewPager)
|
||||||
implementation(androidx.profileinstaller)
|
implementation(libs.androidx.profileInstaller)
|
||||||
|
|
||||||
implementation(androidx.bundles.lifecycle)
|
implementation(libs.bundles.androidx.lifecycle)
|
||||||
|
|
||||||
// Job scheduling
|
// Job scheduling
|
||||||
implementation(androidx.workmanager)
|
implementation(libs.androidx.work)
|
||||||
|
|
||||||
// RxJava
|
// RxJava
|
||||||
implementation(libs.rxjava)
|
implementation(libs.rxJava)
|
||||||
|
|
||||||
// Networking
|
// Networking
|
||||||
implementation(libs.bundles.okhttp)
|
implementation(libs.bundles.okhttp)
|
||||||
implementation(libs.okio)
|
implementation(libs.okio)
|
||||||
implementation(libs.conscrypt.android) // TLS 1.3 support for Android < 10
|
implementation(libs.conscrypt) // TLS 1.3 support for Android < 10
|
||||||
|
|
||||||
// Data serialization (JSON, protobuf, xml)
|
// Data serialization (JSON, protobuf, xml)
|
||||||
implementation(kotlinx.bundles.serialization)
|
implementation(libs.bundles.serialization)
|
||||||
|
|
||||||
// HTML parser
|
// HTML parser
|
||||||
implementation(libs.jsoup)
|
implementation(libs.jsoup)
|
||||||
|
|
||||||
// Disk
|
// Disk
|
||||||
implementation(libs.disklrucache)
|
implementation(libs.diskLruCache)
|
||||||
implementation(libs.unifile)
|
implementation(libs.unifile)
|
||||||
|
|
||||||
// Preferences
|
// Preferences
|
||||||
implementation(libs.preferencektx)
|
implementation(libs.androidx.preference)
|
||||||
|
|
||||||
// Dependency injection
|
// Dependency injection
|
||||||
implementation(libs.injekt)
|
implementation(libs.injekt)
|
||||||
|
implementation(libs.metro.runtime)
|
||||||
|
implementation(libs.metrox.viewmodel)
|
||||||
|
implementation(libs.metrox.viewmodel.compose)
|
||||||
|
|
||||||
// Image loading
|
// Image loading
|
||||||
implementation(platform(libs.coil.bom))
|
|
||||||
implementation(libs.bundles.coil)
|
implementation(libs.bundles.coil)
|
||||||
implementation(libs.subsamplingscaleimageview) {
|
implementation(libs.subsamplingScaleImageView) {
|
||||||
exclude(module = "image-decoder")
|
exclude(module = "image-decoder")
|
||||||
}
|
}
|
||||||
implementation(libs.image.decoder)
|
implementation(libs.image.decoder)
|
||||||
|
|
||||||
// UI libraries
|
// UI libraries
|
||||||
implementation(libs.material)
|
implementation(libs.material)
|
||||||
implementation(libs.flexible.adapter.core)
|
implementation(libs.flexibleAdapter)
|
||||||
implementation(libs.photoview)
|
implementation(libs.photoView)
|
||||||
implementation(libs.directionalviewpager) {
|
implementation(libs.directionalViewPager) {
|
||||||
exclude(group = "androidx.viewpager", module = "viewpager")
|
exclude(group = "androidx.viewpager", module = "viewpager")
|
||||||
}
|
}
|
||||||
implementation(libs.richeditor.compose)
|
implementation(libs.composeRichEditor)
|
||||||
implementation(libs.aboutLibraries.compose)
|
implementation(libs.aboutLibraries.compose)
|
||||||
implementation(libs.bundles.voyager)
|
implementation(libs.bundles.voyager)
|
||||||
implementation(libs.compose.materialmotion)
|
implementation(libs.composeMaterialMotion)
|
||||||
implementation(libs.swipe)
|
implementation(libs.swipe)
|
||||||
implementation(libs.compose.webview)
|
implementation(libs.composeWebview)
|
||||||
implementation(libs.compose.grid)
|
implementation(libs.composeGrid)
|
||||||
implementation(libs.reorderable)
|
implementation(libs.reorderable)
|
||||||
implementation(libs.bundles.markdown)
|
implementation(libs.bundles.markdown)
|
||||||
|
implementation(libs.materialKolor)
|
||||||
|
|
||||||
// Logging
|
// Logging
|
||||||
implementation(libs.logcat)
|
implementation(libs.logcat)
|
||||||
@@ -280,27 +319,133 @@ dependencies {
|
|||||||
// String similarity
|
// String similarity
|
||||||
implementation(libs.stringSimilarity)
|
implementation(libs.stringSimilarity)
|
||||||
|
|
||||||
|
// GraphQL generation
|
||||||
|
implementation(libs.apollo)
|
||||||
|
implementation(libs.apollo.adapters)
|
||||||
|
|
||||||
// Tests
|
// Tests
|
||||||
testImplementation(libs.bundles.test)
|
testImplementation(libs.bundles.test)
|
||||||
testRuntimeOnly(libs.junit.platform.launcher)
|
testRuntimeOnly(libs.junit.platform.launcher)
|
||||||
|
|
||||||
// For detecting memory leaks; see https://square.github.io/leakcanary/
|
// For detecting memory leaks; see https://square.github.io/leakcanary/
|
||||||
// debugImplementation(libs.leakcanary.android)
|
// debugImplementation(libs.leakCanary.android)
|
||||||
implementation(libs.leakcanary.plumber)
|
implementation(libs.leakCanary.plumber)
|
||||||
|
|
||||||
testImplementation(kotlinx.coroutines.test)
|
testImplementation(libs.kotlinx.coroutines.test)
|
||||||
|
}
|
||||||
|
|
||||||
|
apollo {
|
||||||
|
val schemaBasePath = "src/main/graphql/mihon/graphql"
|
||||||
|
service("anilist") {
|
||||||
|
packageName.set("mihon.graphql.anilist")
|
||||||
|
val srcDir = "$schemaBasePath/anilist"
|
||||||
|
srcDir(file(srcDir))
|
||||||
|
|
||||||
|
introspection {
|
||||||
|
endpointUrl.set("https://graphql.anilist.co")
|
||||||
|
schemaFile.set(file("$srcDir/anilist.graphqls"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ISO 3166-1 alpha-2 country code
|
||||||
|
mapScalarToKotlinString("CountryCode")
|
||||||
|
}
|
||||||
|
|
||||||
|
service("kitsu") {
|
||||||
|
packageName.set("mihon.graphql.kitsu")
|
||||||
|
val srcDir = "$schemaBasePath/kitsu"
|
||||||
|
srcDir(file(srcDir))
|
||||||
|
android.defaultConfig.versionCode
|
||||||
|
|
||||||
|
introspection {
|
||||||
|
// Kitsu doesn't like requests without a UA
|
||||||
|
headers.put(
|
||||||
|
"User-Agent",
|
||||||
|
"Mihon v${android.defaultConfig.versionName} (${android.defaultConfig.applicationId})",
|
||||||
|
)
|
||||||
|
endpointUrl.set("https://kitsu.app/api/graphql")
|
||||||
|
schemaFile.set(file("$srcDir/kitsu.graphqls"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// A date, expressed as an ISO8601 string
|
||||||
|
mapScalarToKotlinString("Date")
|
||||||
|
// An ISO 8601-encoded datetime
|
||||||
|
mapScalar("ISO8601DateTime", "kotlin.time.Instant", "com.apollographql.adapter.core.KotlinInstantAdapter")
|
||||||
|
// A loose key-value map in GraphQL
|
||||||
|
mapScalar(
|
||||||
|
"Map",
|
||||||
|
"kotlin.collections.Map<String, Any?>",
|
||||||
|
"mihon.graphql.kitsu.adapter.KitsuMapAdapter",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
service("shikimori") {
|
||||||
|
packageName.set("mihon.graphql.shikimori")
|
||||||
|
val srcDir = "$schemaBasePath/shikimori"
|
||||||
|
srcDir(file(srcDir))
|
||||||
|
|
||||||
|
introspection {
|
||||||
|
endpointUrl.set("https://shikimori.io/api/graphql")
|
||||||
|
schemaFile.set(file("$srcDir/shikimori.graphqls"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// An ISO 8601-encoded date
|
||||||
|
mapScalarToKotlinString("ISO8601Date")
|
||||||
|
}
|
||||||
|
|
||||||
|
service("suwayomi") {
|
||||||
|
packageName.set("mihon.graphql.suwayomi")
|
||||||
|
val srcDir = "$schemaBasePath/suwayomi"
|
||||||
|
srcDir(file(srcDir))
|
||||||
|
|
||||||
|
introspection {
|
||||||
|
endpointUrl.set("http://localhost:4567/api/graphql")
|
||||||
|
schemaFile.set(file("$srcDir/suwayomi.graphqls"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val latestCommitCount = getLatestCommitCount()
|
||||||
|
val latestCommitSha = getLatestCommitSha()
|
||||||
|
val latestCommitTime = getLatestCommitTime()
|
||||||
|
val currentTime = getCurrentTime()
|
||||||
|
|
||||||
|
fun ApplicationVariant.buildConfigField(type: String, name: String, value: Provider<String>) {
|
||||||
|
buildConfigFields?.put(name, value.map { BuildConfigField(type, it, null) })
|
||||||
}
|
}
|
||||||
|
|
||||||
androidComponents {
|
androidComponents {
|
||||||
|
onVariants { variant ->
|
||||||
|
val isUnstableBuild = variant.buildType == "debug" || variant.buildType == "nightly"
|
||||||
|
val buildTime = if (isUnstableBuild) currentTime else latestCommitTime
|
||||||
|
|
||||||
|
variant.buildConfigField("String", "COMMIT_COUNT", latestCommitCount.map { "\"$it\"" })
|
||||||
|
variant.buildConfigField("String", "COMMIT_SHA", latestCommitSha.map { "\"$it\"" })
|
||||||
|
variant.buildConfigField("String", "BUILD_TIME", buildTime.map { "\"$it\"" })
|
||||||
|
|
||||||
|
if (isUnstableBuild) {
|
||||||
|
variant.outputs.forEach { output ->
|
||||||
|
val versionName = output.versionName.get()
|
||||||
|
output.versionName.set(latestCommitCount.map { "$versionName-$it" })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onVariants { variant ->
|
||||||
|
val resSource = variant.sources.res ?: return@onVariants
|
||||||
|
|
||||||
|
val variantName = variant.name.replaceFirstChar { it.uppercase() }
|
||||||
|
val replaceShortcutsPlaceholderTask = tasks.register<ReplaceShortcutsPlaceholderTask>(
|
||||||
|
"replace${variantName}ShortcutPlaceholder",
|
||||||
|
) {
|
||||||
|
applicationId.set(variant.applicationId)
|
||||||
|
shortcutsFile.set(projectDir.resolve("src/main/shortcuts.xml"))
|
||||||
|
}
|
||||||
|
resSource.addGeneratedSourceDirectory(replaceShortcutsPlaceholderTask) { it.outputDir }
|
||||||
|
}
|
||||||
|
|
||||||
onVariants(selector().withFlavor("default" to "standard")) {
|
onVariants(selector().withFlavor("default" to "standard")) {
|
||||||
// Only excluding in standard flavor because this breaks
|
// Only excluding in standard flavor because this breaks
|
||||||
// Layout Inspector's Compose tree
|
// Layout Inspector's Compose tree
|
||||||
it.packaging.resources.excludes.add("META-INF/*.version")
|
it.packaging.resources.excludes.add("META-INF/*.version")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
buildscript {
|
|
||||||
dependencies {
|
|
||||||
classpath(kotlinx.gradle)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
Vendored
+21
-10
@@ -1,8 +1,8 @@
|
|||||||
-dontobfuscate
|
-dontobfuscate
|
||||||
|
|
||||||
-keep,allowoptimization class eu.kanade.**
|
# Injekt reads the injected type from the generic superclass of its type tokens
|
||||||
-keep,allowoptimization class tachiyomi.**
|
-keepattributes Signature
|
||||||
-keep,allowoptimization class mihon.**
|
-keep,allowshrinking class * extends uy.kohesive.injekt.api.FullTypeReference
|
||||||
|
|
||||||
# Keep common dependencies used in extensions
|
# Keep common dependencies used in extensions
|
||||||
-keep,allowoptimization class androidx.preference.** { public protected *; }
|
-keep,allowoptimization class androidx.preference.** { public protected *; }
|
||||||
@@ -16,14 +16,16 @@
|
|||||||
-keep,allowoptimization class rx.** { public protected *; }
|
-keep,allowoptimization class rx.** { public protected *; }
|
||||||
-keep,allowoptimization class app.cash.quickjs.** { public protected *; }
|
-keep,allowoptimization class app.cash.quickjs.** { public protected *; }
|
||||||
-keep,allowoptimization class uy.kohesive.injekt.** { public protected *; }
|
-keep,allowoptimization class uy.kohesive.injekt.** { public protected *; }
|
||||||
|
-keep,allowoptimization class com.squareup.zstd.** { public protected *; }
|
||||||
|
|
||||||
# From extensions-lib
|
-keepclassmembers class * implements java.io.Serializable {
|
||||||
-keep,allowoptimization class eu.kanade.tachiyomi.network.interceptor.RateLimitInterceptorKt { public protected *; }
|
java.lang.Object writeReplace();
|
||||||
-keep,allowoptimization class eu.kanade.tachiyomi.network.interceptor.SpecificHostRateLimitInterceptorKt { public protected *; }
|
java.lang.Object readResolve();
|
||||||
-keep,allowoptimization class eu.kanade.tachiyomi.network.NetworkHelper { public protected *; }
|
}
|
||||||
-keep,allowoptimization class eu.kanade.tachiyomi.network.OkHttpExtensionsKt { public protected *; }
|
|
||||||
-keep,allowoptimization class eu.kanade.tachiyomi.network.RequestsKt { public protected *; }
|
# Provided by the OEM at runtime when supported
|
||||||
-keep,allowoptimization class eu.kanade.tachiyomi.AppInfo { public protected *; }
|
-dontwarn androidx.window.extensions.**
|
||||||
|
-dontwarn androidx.window.sidecar.**
|
||||||
|
|
||||||
##---------------Begin: proguard configuration for RxJava 1.x ----------
|
##---------------Begin: proguard configuration for RxJava 1.x ----------
|
||||||
-dontwarn sun.misc.**
|
-dontwarn sun.misc.**
|
||||||
@@ -80,3 +82,12 @@
|
|||||||
# Firebase
|
# Firebase
|
||||||
-keep class com.google.firebase.installations.** { *; }
|
-keep class com.google.firebase.installations.** { *; }
|
||||||
-keep interface com.google.firebase.installations.** { *; }
|
-keep interface com.google.firebase.installations.** { *; }
|
||||||
|
|
||||||
|
# KotlinX Datetime
|
||||||
|
-keep,allowoptimization class kotlinx.datetime.** { public protected *; }
|
||||||
|
|
||||||
|
# Methods called by Shizuku only
|
||||||
|
-keepclassmembers class mihon.app.shizuku.ShellInterface {
|
||||||
|
public <init>();
|
||||||
|
public void destroy();
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="accent_blue">#7EBBED</color>
|
||||||
|
<color name="icon_background">#2E3943</color>
|
||||||
|
</resources>
|
||||||
@@ -60,8 +60,8 @@
|
|||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
<!-- Deep link to add repos -->
|
<!-- Legacy deeplink to add extension store -->
|
||||||
<intent-filter android:label="@string/action_add_repo">
|
<intent-filter android:label="@string/action_addExtensionStore">
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
@@ -71,6 +71,17 @@
|
|||||||
<data android:host="add-repo" />
|
<data android:host="add-repo" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
|
<!-- Deeplink to add extension store -->
|
||||||
|
<intent-filter android:label="@string/action_addExtensionStore">
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
|
<data android:scheme="mihon" />
|
||||||
|
<data android:host="extension-store" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
<!-- Open backup files -->
|
<!-- Open backup files -->
|
||||||
<intent-filter android:label="@string/pref_restore_backup">
|
<intent-filter android:label="@string/pref_restore_backup">
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
@@ -130,6 +141,10 @@
|
|||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
<data android:mimeType="text/plain" />
|
<data android:mimeType="text/plain" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
<intent-filter android:label="@string/app_settings">
|
||||||
|
<action android:name="android.intent.action.APPLICATION_PREFERENCES" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.app.searchable"
|
android:name="android.app.searchable"
|
||||||
@@ -178,8 +193,21 @@
|
|||||||
|
|
||||||
<data android:host="anilist-auth" />
|
<data android:host="anilist-auth" />
|
||||||
<data android:host="bangumi-auth" />
|
<data android:host="bangumi-auth" />
|
||||||
|
<data android:host="mangabaka-auth" />
|
||||||
<data android:host="myanimelist-auth" />
|
<data android:host="myanimelist-auth" />
|
||||||
<data android:host="shikimori-auth" />
|
<data android:host="shikimori-auth" />
|
||||||
|
<data android:host="hikka-auth" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
|
<!-- Comick requires native reverse-domain schemes -->
|
||||||
|
<data android:scheme="app.mihon" />
|
||||||
|
<data android:host="comick-auth" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package mihon.app.shizuku;
|
package mihon.app.shizuku;
|
||||||
|
|
||||||
interface IShellInterface {
|
interface IShellInterface {
|
||||||
void install(in AssetFileDescriptor apk) = 1;
|
void install(in AssetFileDescriptor apk, in IntentSender intentSender) = 1;
|
||||||
|
|
||||||
void destroy() = 16777114;
|
void destroy() = 16777114;
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,15 @@
|
|||||||
|
mutation AniListAddManga(
|
||||||
|
$manga_id: Int!,
|
||||||
|
$progress: Int!,
|
||||||
|
$status: MediaListStatus!,
|
||||||
|
$private: Boolean!,
|
||||||
|
) {
|
||||||
|
SaveMediaListEntry (
|
||||||
|
mediaId: $manga_id,
|
||||||
|
progress: $progress,
|
||||||
|
status: $status,
|
||||||
|
private: $private,
|
||||||
|
) {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
mutation AniListDeleteManga(
|
||||||
|
$library_id: Int!,
|
||||||
|
) {
|
||||||
|
DeleteMediaListEntry(
|
||||||
|
id: $library_id,
|
||||||
|
) {
|
||||||
|
deleted
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
query AniListGetCurrentUser {
|
||||||
|
Viewer {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
mediaListOptions {
|
||||||
|
scoreFormat
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
query AniListGetLibManga(
|
||||||
|
$user_id: Int!,
|
||||||
|
$manga_id: Int!,
|
||||||
|
) {
|
||||||
|
Page {
|
||||||
|
mediaList(
|
||||||
|
userId: $user_id,
|
||||||
|
type: MANGA,
|
||||||
|
mediaId: $manga_id,
|
||||||
|
) {
|
||||||
|
id
|
||||||
|
status
|
||||||
|
scoreRaw: score(format: POINT_100)
|
||||||
|
progress
|
||||||
|
private
|
||||||
|
startedAt {
|
||||||
|
year
|
||||||
|
month
|
||||||
|
day
|
||||||
|
}
|
||||||
|
completedAt {
|
||||||
|
year
|
||||||
|
month
|
||||||
|
day
|
||||||
|
}
|
||||||
|
media {
|
||||||
|
...MangaFragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
query AniListGetMangaDetails(
|
||||||
|
$manga_id: Int!,
|
||||||
|
) {
|
||||||
|
Page (perPage: 1) {
|
||||||
|
media(
|
||||||
|
id: $manga_id,
|
||||||
|
type: MANGA,
|
||||||
|
format_not_in: [NOVEL],
|
||||||
|
) {
|
||||||
|
...MangaFragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
query AniListSearchManga(
|
||||||
|
$search: String!,
|
||||||
|
) {
|
||||||
|
Page (perPage: 50) {
|
||||||
|
media(
|
||||||
|
search: $search,
|
||||||
|
type: MANGA,
|
||||||
|
format_not_in: [NOVEL],
|
||||||
|
) {
|
||||||
|
...MangaFragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
mutation AniListUpdateManga(
|
||||||
|
$library_id: Int!,
|
||||||
|
$progress: Int!,
|
||||||
|
$status: MediaListStatus!,
|
||||||
|
$private: Boolean!,
|
||||||
|
$score: Int!,
|
||||||
|
$startedAt: FuzzyDateInput!,
|
||||||
|
$completedAt: FuzzyDateInput!,
|
||||||
|
) {
|
||||||
|
SaveMediaListEntry(
|
||||||
|
id: $library_id,
|
||||||
|
progress: $progress,
|
||||||
|
status: $status,
|
||||||
|
private: $private,
|
||||||
|
scoreRaw: $score,
|
||||||
|
startedAt: $startedAt,
|
||||||
|
completedAt: $completedAt,
|
||||||
|
) {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
fragment MangaFragment on Media {
|
||||||
|
id
|
||||||
|
staff {
|
||||||
|
edges {
|
||||||
|
role
|
||||||
|
node {
|
||||||
|
name {
|
||||||
|
userPreferred
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
title {
|
||||||
|
userPreferred
|
||||||
|
}
|
||||||
|
coverImage {
|
||||||
|
large
|
||||||
|
}
|
||||||
|
format
|
||||||
|
countryOfOrigin
|
||||||
|
status
|
||||||
|
chapters
|
||||||
|
description
|
||||||
|
startDate {
|
||||||
|
year
|
||||||
|
month
|
||||||
|
day
|
||||||
|
}
|
||||||
|
averageScore
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,27 @@
|
|||||||
|
mutation KitsuAddLibManga(
|
||||||
|
$media_id: ID!,
|
||||||
|
$status: LibraryEntryStatusEnum!,
|
||||||
|
$progress: Int!,
|
||||||
|
$private: Boolean!,
|
||||||
|
$rating: Int,
|
||||||
|
) {
|
||||||
|
libraryEntry {
|
||||||
|
create(
|
||||||
|
input: {
|
||||||
|
mediaId: $media_id,
|
||||||
|
mediaType: MANGA,
|
||||||
|
status: $status,
|
||||||
|
progress: $progress,
|
||||||
|
private: $private,
|
||||||
|
rating: $rating,
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
errors {
|
||||||
|
message
|
||||||
|
}
|
||||||
|
libraryEntry {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
mutation KitsuDeleteLibEntry(
|
||||||
|
$library_id: ID!,
|
||||||
|
) {
|
||||||
|
libraryEntry {
|
||||||
|
delete(
|
||||||
|
input: {
|
||||||
|
id: $library_id,
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
errors {
|
||||||
|
message
|
||||||
|
}
|
||||||
|
libraryEntry {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
query KitsuFindLibManga(
|
||||||
|
$remote_id: ID!,
|
||||||
|
) {
|
||||||
|
findMangaById(
|
||||||
|
id: $remote_id,
|
||||||
|
) {
|
||||||
|
...MangaFragment
|
||||||
|
myLibraryEntry {
|
||||||
|
id
|
||||||
|
private
|
||||||
|
progress
|
||||||
|
rating
|
||||||
|
status
|
||||||
|
startedAt
|
||||||
|
finishedAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
query KitsuGetCurrentAccount {
|
||||||
|
currentAccount {
|
||||||
|
id
|
||||||
|
ratingSystem
|
||||||
|
profile {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
query KitsuGetMangaDetailsById(
|
||||||
|
$id: ID!,
|
||||||
|
) {
|
||||||
|
findMangaById(
|
||||||
|
id: $id,
|
||||||
|
) {
|
||||||
|
...MangaFragment
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
query KitsuGetMangaDetailsBySlug(
|
||||||
|
$slug: String!,
|
||||||
|
) {
|
||||||
|
findMangaBySlug(
|
||||||
|
slug: $slug,
|
||||||
|
) {
|
||||||
|
...MangaFragment
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
query KitsuSearchMangaByTitle(
|
||||||
|
$query: String!,
|
||||||
|
) {
|
||||||
|
searchMangaByTitle(
|
||||||
|
title: $query,
|
||||||
|
first: 20,
|
||||||
|
) {
|
||||||
|
nodes {
|
||||||
|
...MangaFragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
mutation KitsuUpdateLibManga(
|
||||||
|
$library_id: ID!,
|
||||||
|
$status: LibraryEntryStatusEnum!,
|
||||||
|
$progress: Int!,
|
||||||
|
$private: Boolean!,
|
||||||
|
$rating: Int,
|
||||||
|
$startedAt: ISO8601DateTime,
|
||||||
|
$finishedAt: ISO8601DateTime,
|
||||||
|
) {
|
||||||
|
libraryEntry {
|
||||||
|
update(
|
||||||
|
input: {
|
||||||
|
id: $library_id,
|
||||||
|
status: $status,
|
||||||
|
progress: $progress,
|
||||||
|
private: $private,
|
||||||
|
rating: $rating,
|
||||||
|
startedAt: $startedAt,
|
||||||
|
finishedAt: $finishedAt,
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
errors {
|
||||||
|
message
|
||||||
|
}
|
||||||
|
libraryEntry {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
fragment MangaFragment on Manga {
|
||||||
|
id
|
||||||
|
titles {
|
||||||
|
preferred
|
||||||
|
}
|
||||||
|
chapterCount
|
||||||
|
staff(first: 5) {
|
||||||
|
nodes {
|
||||||
|
role
|
||||||
|
person {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
posterImage {
|
||||||
|
views(names: "small") {
|
||||||
|
name
|
||||||
|
url
|
||||||
|
}
|
||||||
|
original {
|
||||||
|
name
|
||||||
|
url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
description(locales: "en")
|
||||||
|
status
|
||||||
|
subtype
|
||||||
|
startDate
|
||||||
|
slug
|
||||||
|
averageRating
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,22 @@
|
|||||||
|
fragment MangaFragment on Manga {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
chapters
|
||||||
|
kind
|
||||||
|
poster {
|
||||||
|
mainUrl
|
||||||
|
}
|
||||||
|
score
|
||||||
|
url
|
||||||
|
status
|
||||||
|
airedOn {
|
||||||
|
date
|
||||||
|
}
|
||||||
|
description
|
||||||
|
personRoles {
|
||||||
|
person {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
rolesEn
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
query ShikimoriGetCurrentUser {
|
||||||
|
currentUser {
|
||||||
|
id
|
||||||
|
nickname
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
query ShikimoriGetLibManga(
|
||||||
|
$remote_id: String!,
|
||||||
|
) {
|
||||||
|
mangas(
|
||||||
|
ids: $remote_id,
|
||||||
|
limit: 1,
|
||||||
|
) {
|
||||||
|
id
|
||||||
|
url
|
||||||
|
name
|
||||||
|
chapters
|
||||||
|
userRate {
|
||||||
|
id
|
||||||
|
chapters
|
||||||
|
status
|
||||||
|
score
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
query ShikimoriGetMangaDetails(
|
||||||
|
$query: String!,
|
||||||
|
) {
|
||||||
|
mangas(
|
||||||
|
ids: $query,
|
||||||
|
limit: 1,
|
||||||
|
kind: "!light_novel,!novel",
|
||||||
|
) {
|
||||||
|
...MangaFragment
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
query ShikimoriSearchManga(
|
||||||
|
$search: String!
|
||||||
|
) {
|
||||||
|
mangas(
|
||||||
|
search: $search,
|
||||||
|
limit: 20,
|
||||||
|
kind: "!light_novel,!novel",
|
||||||
|
) {
|
||||||
|
...MangaFragment
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,15 @@
|
|||||||
|
fragment MangaFragment on MangaType {
|
||||||
|
description
|
||||||
|
id
|
||||||
|
status
|
||||||
|
thumbnailUrl
|
||||||
|
title
|
||||||
|
url
|
||||||
|
chapters {
|
||||||
|
totalCount
|
||||||
|
}
|
||||||
|
latestReadChapter {
|
||||||
|
chapterNumber
|
||||||
|
}
|
||||||
|
unreadCount
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
query SuwayomiGetManga(
|
||||||
|
$mangaId: Int!,
|
||||||
|
) {
|
||||||
|
manga(
|
||||||
|
id: $mangaId,
|
||||||
|
) {
|
||||||
|
...MangaFragment
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
query SuwayomiGetMangaUnreadChapters(
|
||||||
|
$mangaId: Int!,
|
||||||
|
$chapterNumber: Float!,
|
||||||
|
) {
|
||||||
|
chapters(
|
||||||
|
condition: {
|
||||||
|
mangaId: $mangaId,
|
||||||
|
isRead: false,
|
||||||
|
},
|
||||||
|
filter: {
|
||||||
|
and: [
|
||||||
|
{
|
||||||
|
chapterNumber: {
|
||||||
|
lessThanOrEqualTo: $chapterNumber
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
chapterNumber: {
|
||||||
|
greaterThan: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
){
|
||||||
|
nodes {
|
||||||
|
id
|
||||||
|
chapterNumber
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
mutation SuwayomiMarkChaptersRead(
|
||||||
|
$chapters: [Int!]!,
|
||||||
|
) {
|
||||||
|
updateChapters(
|
||||||
|
input: {
|
||||||
|
ids: $chapters,
|
||||||
|
patch: {
|
||||||
|
isRead: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
__typename
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
mutation SuwayomiMarkAndDeleteChapters(
|
||||||
|
$chapters: [Int!]!,
|
||||||
|
) {
|
||||||
|
updateChapters(
|
||||||
|
input: {
|
||||||
|
ids: $chapters,
|
||||||
|
patch: {
|
||||||
|
isRead: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
__typename
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteDownloadedChapters(
|
||||||
|
input: {
|
||||||
|
ids: $chapters,
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
__typename
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
mutation SuwayomiUpdateMangaProgress(
|
||||||
|
$mangaId: Int!,
|
||||||
|
) {
|
||||||
|
trackProgress(
|
||||||
|
input: {
|
||||||
|
mangaId: $mangaId,
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
__typename
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,13 +0,0 @@
|
|||||||
package eu.kanade.core.util
|
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.collectAsState
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import tachiyomi.domain.source.service.SourceManager
|
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun ifSourcesLoaded(): Boolean {
|
|
||||||
return remember { Injekt.get<SourceManager>().isInitialized }.collectAsState().value
|
|
||||||
}
|
|
||||||
@@ -1,207 +0,0 @@
|
|||||||
package eu.kanade.domain
|
|
||||||
|
|
||||||
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.download.interactor.DeleteDownload
|
|
||||||
import eu.kanade.domain.extension.interactor.GetExtensionLanguages
|
|
||||||
import eu.kanade.domain.extension.interactor.GetExtensionSources
|
|
||||||
import eu.kanade.domain.extension.interactor.GetExtensionsByType
|
|
||||||
import eu.kanade.domain.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.GetIncognitoState
|
|
||||||
import eu.kanade.domain.source.interactor.GetLanguagesWithSources
|
|
||||||
import eu.kanade.domain.source.interactor.GetSourcesWithFavoriteCount
|
|
||||||
import eu.kanade.domain.source.interactor.SetMigrateSorting
|
|
||||||
import eu.kanade.domain.source.interactor.ToggleIncognito
|
|
||||||
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.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 mihon.data.repository.ExtensionRepoRepositoryImpl
|
|
||||||
import mihon.domain.chapter.interactor.FilterChaptersForDownload
|
|
||||||
import mihon.domain.extensionrepo.interactor.CreateExtensionRepo
|
|
||||||
import mihon.domain.extensionrepo.interactor.DeleteExtensionRepo
|
|
||||||
import mihon.domain.extensionrepo.interactor.GetExtensionRepo
|
|
||||||
import mihon.domain.extensionrepo.interactor.GetExtensionRepoCount
|
|
||||||
import mihon.domain.extensionrepo.interactor.ReplaceExtensionRepo
|
|
||||||
import mihon.domain.extensionrepo.interactor.UpdateExtensionRepo
|
|
||||||
import mihon.domain.extensionrepo.repository.ExtensionRepoRepository
|
|
||||||
import mihon.domain.extensionrepo.service.ExtensionRepoService
|
|
||||||
import mihon.domain.migration.usecases.MigrateMangaUseCase
|
|
||||||
import mihon.domain.upcoming.interactor.GetUpcomingManga
|
|
||||||
import tachiyomi.data.category.CategoryRepositoryImpl
|
|
||||||
import tachiyomi.data.chapter.ChapterRepositoryImpl
|
|
||||||
import tachiyomi.data.history.HistoryRepositoryImpl
|
|
||||||
import tachiyomi.data.manga.MangaRepositoryImpl
|
|
||||||
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.interactor.UpdateMangaNotes
|
|
||||||
import tachiyomi.domain.manga.repository.MangaRepository
|
|
||||||
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
|
|
||||||
import uy.kohesive.injekt.api.InjektModule
|
|
||||||
import uy.kohesive.injekt.api.InjektRegistrar
|
|
||||||
import uy.kohesive.injekt.api.addFactory
|
|
||||||
import uy.kohesive.injekt.api.addSingletonFactory
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
|
|
||||||
class DomainModule : InjektModule {
|
|
||||||
|
|
||||||
override fun InjektRegistrar.registerInjectables() {
|
|
||||||
addSingletonFactory<CategoryRepository> { CategoryRepositoryImpl(get()) }
|
|
||||||
addFactory { GetCategories(get()) }
|
|
||||||
addFactory { ResetCategoryFlags(get(), get()) }
|
|
||||||
addFactory { SetDisplayMode(get()) }
|
|
||||||
addFactory { SetSortModeForCategory(get(), get()) }
|
|
||||||
addFactory { CreateCategoryWithName(get(), get()) }
|
|
||||||
addFactory { RenameCategory(get()) }
|
|
||||||
addFactory { ReorderCategory(get()) }
|
|
||||||
addFactory { UpdateCategory(get()) }
|
|
||||||
addFactory { DeleteCategory(get(), get(), get()) }
|
|
||||||
|
|
||||||
addSingletonFactory<MangaRepository> { MangaRepositoryImpl(get()) }
|
|
||||||
addFactory { GetDuplicateLibraryManga(get()) }
|
|
||||||
addFactory { GetFavorites(get()) }
|
|
||||||
addFactory { GetLibraryManga(get()) }
|
|
||||||
addFactory { GetMangaWithChapters(get(), get()) }
|
|
||||||
addFactory { GetMangaByUrlAndSourceId(get()) }
|
|
||||||
addFactory { GetManga(get()) }
|
|
||||||
addFactory { GetNextChapters(get(), get(), get()) }
|
|
||||||
addFactory { GetUpcomingManga(get()) }
|
|
||||||
addFactory { ResetViewerFlags(get()) }
|
|
||||||
addFactory { SetMangaChapterFlags(get()) }
|
|
||||||
addFactory { FetchInterval(get()) }
|
|
||||||
addFactory { SetMangaDefaultChapterFlags(get(), get(), get()) }
|
|
||||||
addFactory { SetMangaViewerFlags(get()) }
|
|
||||||
addFactory { NetworkToLocalManga(get()) }
|
|
||||||
addFactory { UpdateManga(get(), get()) }
|
|
||||||
addFactory { UpdateMangaNotes(get()) }
|
|
||||||
addFactory { SetMangaCategories(get()) }
|
|
||||||
addFactory { GetExcludedScanlators(get()) }
|
|
||||||
addFactory { SetExcludedScanlators(get()) }
|
|
||||||
addFactory {
|
|
||||||
MigrateMangaUseCase(
|
|
||||||
get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), 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 { GetChaptersByMangaId(get()) }
|
|
||||||
addFactory { GetChapterByUrlAndMangaId(get()) }
|
|
||||||
addFactory { UpdateChapter(get()) }
|
|
||||||
addFactory { SetReadStatus(get(), get(), get(), get()) }
|
|
||||||
addFactory { ShouldUpdateDbChapter() }
|
|
||||||
addFactory { SyncChaptersWithSource(get(), get(), get(), get(), get(), get(), get(), get(), get()) }
|
|
||||||
addFactory { GetAvailableScanlators(get()) }
|
|
||||||
addFactory { FilterChaptersForDownload(get(), get(), get()) }
|
|
||||||
|
|
||||||
addSingletonFactory<HistoryRepository> { HistoryRepositoryImpl(get()) }
|
|
||||||
addFactory { GetHistory(get()) }
|
|
||||||
addFactory { UpsertHistory(get()) }
|
|
||||||
addFactory { RemoveHistory(get()) }
|
|
||||||
addFactory { GetTotalReadDuration(get()) }
|
|
||||||
|
|
||||||
addFactory { DeleteDownload(get(), get()) }
|
|
||||||
|
|
||||||
addFactory { GetExtensionsByType(get(), get()) }
|
|
||||||
addFactory { GetExtensionSources(get()) }
|
|
||||||
addFactory { GetExtensionLanguages(get(), get()) }
|
|
||||||
|
|
||||||
addSingletonFactory<UpdatesRepository> { UpdatesRepositoryImpl(get()) }
|
|
||||||
addFactory { GetUpdates(get()) }
|
|
||||||
|
|
||||||
addSingletonFactory<SourceRepository> { SourceRepositoryImpl(get(), get()) }
|
|
||||||
addSingletonFactory<StubSourceRepository> { StubSourceRepositoryImpl(get()) }
|
|
||||||
addFactory { GetEnabledSources(get(), get()) }
|
|
||||||
addFactory { GetLanguagesWithSources(get(), get()) }
|
|
||||||
addFactory { GetRemoteManga(get()) }
|
|
||||||
addFactory { GetSourcesWithFavoriteCount(get(), get()) }
|
|
||||||
addFactory { GetSourcesWithNonLibraryManga(get()) }
|
|
||||||
addFactory { SetMigrateSorting(get()) }
|
|
||||||
addFactory { ToggleLanguage(get()) }
|
|
||||||
addFactory { ToggleSource(get()) }
|
|
||||||
addFactory { ToggleSourcePin(get()) }
|
|
||||||
addFactory { TrustExtension(get(), get()) }
|
|
||||||
|
|
||||||
addSingletonFactory<ExtensionRepoRepository> { ExtensionRepoRepositoryImpl(get()) }
|
|
||||||
addFactory { ExtensionRepoService(get(), get()) }
|
|
||||||
addFactory { GetExtensionRepo(get()) }
|
|
||||||
addFactory { GetExtensionRepoCount(get()) }
|
|
||||||
addFactory { CreateExtensionRepo(get(), get()) }
|
|
||||||
addFactory { DeleteExtensionRepo(get()) }
|
|
||||||
addFactory { ReplaceExtensionRepo(get()) }
|
|
||||||
addFactory { UpdateExtensionRepo(get(), get()) }
|
|
||||||
addFactory { ToggleIncognito(get()) }
|
|
||||||
addFactory { GetIncognitoState(get(), get(), get()) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,26 +2,34 @@ package eu.kanade.domain.base
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import dev.icerock.moko.resources.StringResource
|
import dev.icerock.moko.resources.StringResource
|
||||||
|
import dev.zacsweers.metro.AppScope
|
||||||
|
import dev.zacsweers.metro.Inject
|
||||||
|
import dev.zacsweers.metro.SingleIn
|
||||||
import eu.kanade.tachiyomi.util.system.GLUtil
|
import eu.kanade.tachiyomi.util.system.GLUtil
|
||||||
import tachiyomi.core.common.preference.Preference
|
import tachiyomi.core.common.preference.Preference
|
||||||
import tachiyomi.core.common.preference.PreferenceStore
|
import tachiyomi.core.common.preference.PreferenceStore
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
@SingleIn(AppScope::class)
|
||||||
class BasePreferences(
|
class BasePreferences(
|
||||||
val context: Context,
|
val context: Context,
|
||||||
private val preferenceStore: PreferenceStore,
|
preferenceStore: PreferenceStore,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun downloadedOnly() = preferenceStore.getBoolean(
|
val downloadedOnly: Preference<Boolean> = preferenceStore.getBoolean(
|
||||||
Preference.appStateKey("pref_downloaded_only"),
|
Preference.appStateKey("pref_downloaded_only"),
|
||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun incognitoMode() = preferenceStore.getBoolean(Preference.appStateKey("incognito_mode"), false)
|
val incognitoMode: Preference<Boolean> = preferenceStore.getBoolean(Preference.appStateKey("incognito_mode"), false)
|
||||||
|
|
||||||
fun extensionInstaller() = ExtensionInstallerPreference(context, preferenceStore)
|
val extensionInstaller: ExtensionInstallerPreference = ExtensionInstallerPreference(context, preferenceStore)
|
||||||
|
|
||||||
fun shownOnboardingFlow() = preferenceStore.getBoolean(Preference.appStateKey("onboarding_complete"), false)
|
val shownOnboardingFlow: Preference<Boolean> = preferenceStore.getBoolean(
|
||||||
|
Preference.appStateKey("onboarding_complete"),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
|
||||||
enum class ExtensionInstaller(val titleRes: StringResource, val requiresSystemPermission: Boolean) {
|
enum class ExtensionInstaller(val titleRes: StringResource, val requiresSystemPermission: Boolean) {
|
||||||
LEGACY(MR.strings.ext_installer_legacy, true),
|
LEGACY(MR.strings.ext_installer_legacy, true),
|
||||||
@@ -30,9 +38,22 @@ class BasePreferences(
|
|||||||
PRIVATE(MR.strings.ext_installer_private, false),
|
PRIVATE(MR.strings.ext_installer_private, false),
|
||||||
}
|
}
|
||||||
|
|
||||||
fun displayProfile() = preferenceStore.getString("pref_display_profile_key", "")
|
val displayProfile: Preference<String> = preferenceStore.getString("pref_display_profile_key", "")
|
||||||
|
|
||||||
fun hardwareBitmapThreshold() = preferenceStore.getInt("pref_hardware_bitmap_threshold", GLUtil.SAFE_TEXTURE_LIMIT)
|
val hardwareBitmapThreshold: Preference<Int> = preferenceStore.getInt(
|
||||||
|
"pref_hardware_bitmap_threshold",
|
||||||
|
GLUtil.SAFE_TEXTURE_LIMIT,
|
||||||
|
)
|
||||||
|
|
||||||
fun alwaysDecodeLongStripWithSSIV() = preferenceStore.getBoolean("pref_always_decode_long_strip_with_ssiv", false)
|
val installationId: Preference<String> = preferenceStore.getString(Preference.appStateKey("installation_id"), "")
|
||||||
|
|
||||||
|
val donationCampaignShown: Preference<Boolean> = preferenceStore.getBoolean(
|
||||||
|
Preference.appStateKey("donation_campaign_shown"),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
|
||||||
|
val skippedAppUpdate: Preference<String> = preferenceStore.getString(
|
||||||
|
Preference.appStateKey("skipped_app_update"),
|
||||||
|
"",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
package eu.kanade.domain.chapter.interactor
|
package eu.kanade.domain.chapter.interactor
|
||||||
|
|
||||||
|
import dev.zacsweers.metro.Inject
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import tachiyomi.domain.chapter.repository.ChapterRepository
|
import tachiyomi.domain.chapter.repository.ChapterRepository
|
||||||
|
|
||||||
|
@Inject
|
||||||
class GetAvailableScanlators(
|
class GetAvailableScanlators(
|
||||||
private val repository: ChapterRepository,
|
private val repository: ChapterRepository,
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
package eu.kanade.domain.chapter.interactor
|
package eu.kanade.domain.chapter.interactor
|
||||||
|
|
||||||
|
import dev.zacsweers.metro.Inject
|
||||||
import eu.kanade.domain.download.interactor.DeleteDownload
|
import eu.kanade.domain.download.interactor.DeleteDownload
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import tachiyomi.core.common.util.lang.withNonCancellableContext
|
|
||||||
import tachiyomi.core.common.util.system.logcat
|
import tachiyomi.core.common.util.system.logcat
|
||||||
import tachiyomi.domain.chapter.model.Chapter
|
import tachiyomi.domain.chapter.model.Chapter
|
||||||
import tachiyomi.domain.chapter.model.ChapterUpdate
|
import tachiyomi.domain.chapter.model.ChapterUpdate
|
||||||
@@ -11,6 +11,7 @@ import tachiyomi.domain.download.service.DownloadPreferences
|
|||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
import tachiyomi.domain.manga.repository.MangaRepository
|
import tachiyomi.domain.manga.repository.MangaRepository
|
||||||
|
|
||||||
|
@Inject
|
||||||
class SetReadStatus(
|
class SetReadStatus(
|
||||||
private val downloadPreferences: DownloadPreferences,
|
private val downloadPreferences: DownloadPreferences,
|
||||||
private val deleteDownload: DeleteDownload,
|
private val deleteDownload: DeleteDownload,
|
||||||
@@ -19,14 +20,13 @@ class SetReadStatus(
|
|||||||
) {
|
) {
|
||||||
|
|
||||||
private val mapper = { chapter: Chapter, read: Boolean ->
|
private val mapper = { chapter: Chapter, read: Boolean ->
|
||||||
ChapterUpdate(
|
ChapterUpdate(chapter.id) {
|
||||||
read = read,
|
this.read = read
|
||||||
lastPageRead = if (!read) 0 else null,
|
if (!read) lastPageRead = 0
|
||||||
id = chapter.id,
|
}
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun await(read: Boolean, vararg chapters: Chapter): Result = withNonCancellableContext {
|
suspend fun await(read: Boolean, vararg chapters: Chapter): Result {
|
||||||
val chaptersToUpdate = chapters.filter {
|
val chaptersToUpdate = chapters.filter {
|
||||||
when (read) {
|
when (read) {
|
||||||
true -> !it.read
|
true -> !it.read
|
||||||
@@ -34,7 +34,7 @@ class SetReadStatus(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (chaptersToUpdate.isEmpty()) {
|
if (chaptersToUpdate.isEmpty()) {
|
||||||
return@withNonCancellableContext Result.NoChapters
|
return Result.NoChapters
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -43,11 +43,14 @@ class SetReadStatus(
|
|||||||
)
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logcat(LogPriority.ERROR, e)
|
logcat(LogPriority.ERROR, e)
|
||||||
return@withNonCancellableContext Result.InternalError(e)
|
return Result.InternalError(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (read && downloadPreferences.removeAfterMarkedAsRead().get()) {
|
if (read && downloadPreferences.removeAfterMarkedAsRead.get()) {
|
||||||
chaptersToUpdate
|
chaptersToUpdate
|
||||||
|
// Chapters here still carry their pre-update read state; downstream
|
||||||
|
// deletion filtering (excluded categories) relies on the new state.
|
||||||
|
.map { it.copy(read = true) }
|
||||||
.groupBy { it.mangaId }
|
.groupBy { it.mangaId }
|
||||||
.forEach { (mangaId, chapters) ->
|
.forEach { (mangaId, chapters) ->
|
||||||
deleteDownload.awaitAll(
|
deleteDownload.awaitAll(
|
||||||
@@ -57,17 +60,16 @@ class SetReadStatus(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Result.Success
|
return Result.Success
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun await(mangaId: Long, read: Boolean): Result = withNonCancellableContext {
|
suspend fun await(mangaId: Long, read: Boolean): Result =
|
||||||
await(
|
await(
|
||||||
read = read,
|
read = read,
|
||||||
chapters = chapterRepository
|
chapters = chapterRepository
|
||||||
.getChapterByMangaId(mangaId)
|
.getChapterByMangaId(mangaId)
|
||||||
.toTypedArray(),
|
.toTypedArray(),
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun await(manga: Manga, read: Boolean) =
|
suspend fun await(manga: Manga, read: Boolean) =
|
||||||
await(manga.id, read)
|
await(manga.id, read)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package eu.kanade.domain.chapter.interactor
|
package eu.kanade.domain.chapter.interactor
|
||||||
|
|
||||||
|
import dev.zacsweers.metro.Inject
|
||||||
import eu.kanade.domain.chapter.model.copyFromSChapter
|
import eu.kanade.domain.chapter.model.copyFromSChapter
|
||||||
import eu.kanade.domain.chapter.model.toSChapter
|
import eu.kanade.domain.chapter.model.toSChapter
|
||||||
import eu.kanade.domain.manga.interactor.GetExcludedScanlators
|
import eu.kanade.domain.manga.interactor.GetExcludedScanlators
|
||||||
@@ -10,30 +11,32 @@ import eu.kanade.tachiyomi.data.download.DownloadProvider
|
|||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
|
import kotlinx.datetime.TimeZone
|
||||||
|
import kotlinx.datetime.toInstant
|
||||||
|
import kotlinx.datetime.toLocalDateTime
|
||||||
import tachiyomi.data.chapter.ChapterSanitizer
|
import tachiyomi.data.chapter.ChapterSanitizer
|
||||||
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
|
|
||||||
import tachiyomi.domain.chapter.interactor.ShouldUpdateDbChapter
|
import tachiyomi.domain.chapter.interactor.ShouldUpdateDbChapter
|
||||||
import tachiyomi.domain.chapter.interactor.UpdateChapter
|
|
||||||
import tachiyomi.domain.chapter.model.Chapter
|
import tachiyomi.domain.chapter.model.Chapter
|
||||||
|
import tachiyomi.domain.chapter.model.ChapterRemoteUpdate
|
||||||
import tachiyomi.domain.chapter.model.NoChaptersException
|
import tachiyomi.domain.chapter.model.NoChaptersException
|
||||||
import tachiyomi.domain.chapter.model.toChapterUpdate
|
|
||||||
import tachiyomi.domain.chapter.repository.ChapterRepository
|
import tachiyomi.domain.chapter.repository.ChapterRepository
|
||||||
import tachiyomi.domain.chapter.service.ChapterRecognition
|
import tachiyomi.domain.chapter.service.ChapterRecognition
|
||||||
import tachiyomi.domain.library.service.LibraryPreferences
|
import tachiyomi.domain.library.service.LibraryPreferences
|
||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
import tachiyomi.source.local.isLocal
|
import tachiyomi.source.local.isLocal
|
||||||
import java.lang.Long.max
|
import java.lang.Long.max
|
||||||
import java.time.ZonedDateTime
|
|
||||||
import java.util.TreeSet
|
import java.util.TreeSet
|
||||||
|
import kotlin.time.Clock
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
import kotlin.time.Instant
|
||||||
|
|
||||||
|
@Inject
|
||||||
class SyncChaptersWithSource(
|
class SyncChaptersWithSource(
|
||||||
private val downloadManager: DownloadManager,
|
private val downloadManager: DownloadManager,
|
||||||
private val downloadProvider: DownloadProvider,
|
private val downloadProvider: DownloadProvider,
|
||||||
private val chapterRepository: ChapterRepository,
|
private val chapterRepository: ChapterRepository,
|
||||||
private val shouldUpdateDbChapter: ShouldUpdateDbChapter,
|
private val shouldUpdateDbChapter: ShouldUpdateDbChapter,
|
||||||
private val updateManga: UpdateManga,
|
private val updateManga: UpdateManga,
|
||||||
private val updateChapter: UpdateChapter,
|
|
||||||
private val getChaptersByMangaId: GetChaptersByMangaId,
|
|
||||||
private val getExcludedScanlators: GetExcludedScanlators,
|
private val getExcludedScanlators: GetExcludedScanlators,
|
||||||
private val libraryPreferences: LibraryPreferences,
|
private val libraryPreferences: LibraryPreferences,
|
||||||
) {
|
) {
|
||||||
@@ -51,14 +54,15 @@ class SyncChaptersWithSource(
|
|||||||
manga: Manga,
|
manga: Manga,
|
||||||
source: Source,
|
source: Source,
|
||||||
manualFetch: Boolean = false,
|
manualFetch: Boolean = false,
|
||||||
fetchWindow: Pair<Long, Long> = Pair(0, 0),
|
fetchWindow: ClosedRange<Instant>? = null,
|
||||||
): List<Chapter> {
|
): List<Chapter> {
|
||||||
if (rawSourceChapters.isEmpty() && !source.isLocal()) {
|
if (rawSourceChapters.isEmpty() && !source.isLocal()) {
|
||||||
throw NoChaptersException()
|
throw NoChaptersException()
|
||||||
}
|
}
|
||||||
|
|
||||||
val now = ZonedDateTime.now()
|
val timeZone = TimeZone.currentSystemDefault()
|
||||||
val nowMillis = now.toInstant().toEpochMilli()
|
val now = Clock.System.now().toLocalDateTime(timeZone)
|
||||||
|
val nowInstant = now.toInstant(timeZone)
|
||||||
|
|
||||||
val sourceChapters = rawSourceChapters
|
val sourceChapters = rawSourceChapters
|
||||||
.distinctBy { it.url }
|
.distinctBy { it.url }
|
||||||
@@ -69,42 +73,46 @@ class SyncChaptersWithSource(
|
|||||||
.copy(mangaId = manga.id, sourceOrder = i.toLong())
|
.copy(mangaId = manga.id, sourceOrder = i.toLong())
|
||||||
}
|
}
|
||||||
|
|
||||||
val dbChapters = getChaptersByMangaId.await(manga.id)
|
val dbChapters = chapterRepository.getChapterByMangaId(manga.id)
|
||||||
|
val dbChaptersByUrl = dbChapters.associateBy { it.url }
|
||||||
|
|
||||||
val newChapters = mutableListOf<Chapter>()
|
val newChapters = mutableListOf<Chapter>()
|
||||||
val updatedChapters = mutableListOf<Chapter>()
|
val updatedChapters = mutableListOf<ChapterRemoteUpdate>()
|
||||||
val removedChapters = dbChapters.filterNot { dbChapter ->
|
val sourceUrls = mutableSetOf<String>()
|
||||||
sourceChapters.any { sourceChapter ->
|
|
||||||
dbChapter.url == sourceChapter.url
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Used to not set upload date of older chapters
|
// Used to not set upload date of older chapters
|
||||||
// to a higher value than newer chapters
|
// to a higher value than newer chapters
|
||||||
var maxSeenUploadDate = 0L
|
var maxSeenUploadDate: Instant? = null
|
||||||
|
|
||||||
for (sourceChapter in sourceChapters) {
|
// Update metadata from source if necessary.
|
||||||
var chapter = sourceChapter
|
val preparedChapters = if (source is HttpSource) {
|
||||||
|
val sManga = manga.toSManga()
|
||||||
// Update metadata from source if necessary.
|
sourceChapters.map { chapter ->
|
||||||
if (source is HttpSource) {
|
|
||||||
val sChapter = chapter.toSChapter()
|
val sChapter = chapter.toSChapter()
|
||||||
source.prepareNewChapter(sChapter, manga.toSManga())
|
@Suppress("DEPRECATION")
|
||||||
chapter = chapter.copyFromSChapter(sChapter)
|
source.prepareNewChapter(sChapter, sManga)
|
||||||
|
chapter.copyFromSChapter(sChapter)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
sourceChapters
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recognize chapter numbers, from the whole list since names alone don't always tell.
|
||||||
|
val recognizedChapters = preparedChapters
|
||||||
|
.zip(ChapterRecognition.parseChapterNumbers(manga.title, preparedChapters)) { chapter, numbering ->
|
||||||
|
chapter.copy(chapterNumber = numbering.chapter?.let(ChapterRecognition::toDouble) ?: -1.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recognize chapter number for the chapter.
|
for (chapter in recognizedChapters) {
|
||||||
val chapterNumber = ChapterRecognition.parseChapterNumber(manga.title, chapter.name, chapter.chapterNumber)
|
if (!sourceUrls.add(chapter.url)) continue
|
||||||
chapter = chapter.copy(chapterNumber = chapterNumber)
|
|
||||||
|
|
||||||
val dbChapter = dbChapters.find { it.url == chapter.url }
|
val dbChapter = dbChaptersByUrl[chapter.url]
|
||||||
|
|
||||||
if (dbChapter == null) {
|
if (dbChapter == null) {
|
||||||
val toAddChapter = if (chapter.dateUpload == 0L) {
|
val toAddChapter = if (chapter.dateUpload == null) {
|
||||||
val altDateUpload = if (maxSeenUploadDate == 0L) nowMillis else maxSeenUploadDate
|
chapter.copy(dateUpload = maxSeenUploadDate ?: nowInstant)
|
||||||
chapter.copy(dateUpload = altDateUpload)
|
|
||||||
} else {
|
} else {
|
||||||
maxSeenUploadDate = max(maxSeenUploadDate, sourceChapter.dateUpload)
|
maxSeenUploadDate = listOfNotNull(maxSeenUploadDate, chapter.dateUpload).maxOrNull()
|
||||||
chapter
|
chapter
|
||||||
}
|
}
|
||||||
newChapters.add(toAddChapter)
|
newChapters.add(toAddChapter)
|
||||||
@@ -123,26 +131,36 @@ class SyncChaptersWithSource(
|
|||||||
downloadManager.renameChapter(source, manga, dbChapter, chapter)
|
downloadManager.renameChapter(source, manga, dbChapter, chapter)
|
||||||
}
|
}
|
||||||
|
|
||||||
var toChangeChapter = dbChapter.copy(
|
updatedChapters.add(
|
||||||
name = chapter.name,
|
ChapterRemoteUpdate(
|
||||||
chapterNumber = chapter.chapterNumber,
|
id = dbChapter.id,
|
||||||
scanlator = chapter.scanlator,
|
name = chapter.name,
|
||||||
sourceOrder = chapter.sourceOrder,
|
chapterNumber = chapter.chapterNumber,
|
||||||
|
scanlator = chapter.scanlator,
|
||||||
|
sourceOrder = chapter.sourceOrder,
|
||||||
|
dateUpload = chapter.dateUpload,
|
||||||
|
memo = chapter.memo,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
if (chapter.dateUpload != 0L) {
|
|
||||||
toChangeChapter = toChangeChapter.copy(dateUpload = chapter.dateUpload)
|
|
||||||
}
|
|
||||||
updatedChapters.add(toChangeChapter)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val removedChapters = dbChapters.filterNot { it.url in sourceUrls }
|
||||||
|
|
||||||
|
// The source no longer lists these, so their queued downloads could only fail
|
||||||
|
removedChapters.mapNotNull { downloadManager.getQueuedDownloadOrNull(it.id) }
|
||||||
|
.takeIf { it.isNotEmpty() }
|
||||||
|
?.let { downloadManager.cancelQueuedDownloads(it) }
|
||||||
|
|
||||||
// Return if there's nothing to add, delete, or update to avoid unnecessary db transactions.
|
// Return if there's nothing to add, delete, or update to avoid unnecessary db transactions.
|
||||||
if (newChapters.isEmpty() && removedChapters.isEmpty() && updatedChapters.isEmpty()) {
|
if (newChapters.isEmpty() && removedChapters.isEmpty() && updatedChapters.isEmpty()) {
|
||||||
if (manualFetch || manga.fetchInterval == 0 || manga.nextUpdate < fetchWindow.first) {
|
val isNextUpdateBeforeWindow = fetchWindow != null &&
|
||||||
|
manga.nextUpdate.let { it == null || it < fetchWindow.start }
|
||||||
|
if (manualFetch || manga.fetchInterval == 0 || isNextUpdateBeforeWindow) {
|
||||||
updateManga.awaitUpdateFetchInterval(
|
updateManga.awaitUpdateFetchInterval(
|
||||||
manga,
|
manga,
|
||||||
|
timeZone,
|
||||||
now,
|
now,
|
||||||
fetchWindow,
|
fetchWindow,
|
||||||
)
|
)
|
||||||
@@ -171,14 +189,14 @@ class SyncChaptersWithSource(
|
|||||||
val deletedChapterNumberDateFetchMap = removedChapters.sortedByDescending { it.dateFetch }
|
val deletedChapterNumberDateFetchMap = removedChapters.sortedByDescending { it.dateFetch }
|
||||||
.associate { it.chapterNumber to it.dateFetch }
|
.associate { it.chapterNumber to it.dateFetch }
|
||||||
|
|
||||||
val markDuplicateAsRead = libraryPreferences.markDuplicateReadChapterAsRead().get()
|
val markDuplicateAsRead = libraryPreferences.markDuplicateReadChapterAsRead.get()
|
||||||
.contains(LibraryPreferences.MARK_DUPLICATE_CHAPTER_READ_NEW)
|
.contains(LibraryPreferences.MARK_DUPLICATE_CHAPTER_READ_NEW)
|
||||||
|
|
||||||
// Date fetch is set in such a way that the upper ones will have bigger value than the lower ones
|
// 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.
|
// Sources MUST return the chapters from most to less recent, which is common.
|
||||||
var itemCount = newChapters.size
|
var itemCount = newChapters.size
|
||||||
var updatedToAdd = newChapters.map { toAddItem ->
|
val toAdd = newChapters.map { toAddItem ->
|
||||||
var chapter = toAddItem.copy(dateFetch = nowMillis + itemCount--)
|
var chapter = toAddItem.copy(dateFetch = nowInstant + (itemCount--).milliseconds)
|
||||||
|
|
||||||
if (chapter.chapterNumber in readChapterNumbers && markDuplicateAsRead) {
|
if (chapter.chapterNumber in readChapterNumbers && markDuplicateAsRead) {
|
||||||
changedOrDuplicateReadUrls.add(chapter.url)
|
changedOrDuplicateReadUrls.add(chapter.url)
|
||||||
@@ -202,27 +220,25 @@ class SyncChaptersWithSource(
|
|||||||
chapter
|
chapter
|
||||||
}
|
}
|
||||||
|
|
||||||
if (removedChapters.isNotEmpty()) {
|
val added = chapterRepository.updateFromRemote(
|
||||||
val toDeleteIds = removedChapters.map { it.id }
|
removedIds = removedChapters.map { it.id },
|
||||||
chapterRepository.removeChaptersWithIds(toDeleteIds)
|
added = toAdd,
|
||||||
}
|
updated = updatedChapters,
|
||||||
|
)
|
||||||
if (updatedToAdd.isNotEmpty()) {
|
|
||||||
updatedToAdd = chapterRepository.addAll(updatedToAdd)
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
updateManga.awaitUpdateLastUpdate(manga.id)
|
|
||||||
|
|
||||||
val excludedScanlators = getExcludedScanlators.await(manga.id).toHashSet()
|
val excludedScanlators = getExcludedScanlators.await(manga.id).toHashSet()
|
||||||
|
|
||||||
return updatedToAdd.filterNot { it.url in changedOrDuplicateReadUrls || it.scanlator in excludedScanlators }
|
// A first fetch lists the back catalogue, and chapters coming back under a deleted number were out already
|
||||||
|
val foundRelease = dbChapters.isNotEmpty() &&
|
||||||
|
toAdd.any { chapter ->
|
||||||
|
chapter.scanlator !in excludedScanlators &&
|
||||||
|
!(chapter.isRecognizedNumber && chapter.chapterNumber in deletedChapterNumbers)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set this manga as updated since chapters were changed
|
||||||
|
// Note that last_update actually represents last time the chapter list changed at all
|
||||||
|
updateManga.awaitUpdateFetchIntervalAndLastUpdate(manga, timeZone, now, fetchWindow, foundRelease)
|
||||||
|
|
||||||
|
return added.filterNot { it.url in changedOrDuplicateReadUrls || it.scanlator in excludedScanlators }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
package eu.kanade.domain.chapter.model
|
package eu.kanade.domain.chapter.model
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.ChapterImpl
|
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
|
import mihon.core.common.extensions.toEpochMillisOrZero
|
||||||
|
import mihon.core.common.extensions.toInstantOrNull
|
||||||
import tachiyomi.domain.chapter.model.Chapter
|
import tachiyomi.domain.chapter.model.Chapter
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter as DbChapter
|
|
||||||
|
|
||||||
// TODO: Remove when all deps are migrated
|
|
||||||
fun Chapter.toSChapter(): SChapter {
|
fun Chapter.toSChapter(): SChapter {
|
||||||
return SChapter.create().also {
|
return SChapter.create().also {
|
||||||
it.url = url
|
it.url = url
|
||||||
it.name = name
|
it.name = name
|
||||||
it.date_upload = dateUpload
|
it.date_upload = dateUpload.toEpochMillisOrZero()
|
||||||
it.chapter_number = chapterNumber.toFloat()
|
it.chapter_number = chapterNumber.toFloat()
|
||||||
it.scanlator = scanlator
|
it.scanlator = scanlator
|
||||||
|
it.memo = memo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -20,23 +20,9 @@ fun Chapter.copyFromSChapter(sChapter: SChapter): Chapter {
|
|||||||
return this.copy(
|
return this.copy(
|
||||||
name = sChapter.name,
|
name = sChapter.name,
|
||||||
url = sChapter.url,
|
url = sChapter.url,
|
||||||
dateUpload = sChapter.date_upload,
|
dateUpload = sChapter.date_upload.toInstantOrNull(),
|
||||||
chapterNumber = sChapter.chapter_number.toDouble(),
|
chapterNumber = sChapter.chapter_number.toDouble(),
|
||||||
scanlator = sChapter.scanlator?.ifBlank { null }?.trim(),
|
scanlator = sChapter.scanlator?.ifBlank { null }?.trim(),
|
||||||
|
memo = sChapter.memo,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Chapter.toDbChapter(): DbChapter = ChapterImpl().also {
|
|
||||||
it.id = id
|
|
||||||
it.manga_id = mangaId
|
|
||||||
it.url = url
|
|
||||||
it.name = name
|
|
||||||
it.scanlator = scanlator
|
|
||||||
it.read = read
|
|
||||||
it.bookmark = bookmark
|
|
||||||
it.last_page_read = lastPageRead.toInt()
|
|
||||||
it.date_fetch = dateFetch
|
|
||||||
it.date_upload = dateUpload
|
|
||||||
it.chapter_number = chapterNumber.toFloat()
|
|
||||||
it.source_order = sourceOrder.toInt()
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,17 +1,18 @@
|
|||||||
package eu.kanade.domain.download.interactor
|
package eu.kanade.domain.download.interactor
|
||||||
|
|
||||||
|
import dev.zacsweers.metro.Inject
|
||||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||||
import tachiyomi.core.common.util.lang.withNonCancellableContext
|
|
||||||
import tachiyomi.domain.chapter.model.Chapter
|
import tachiyomi.domain.chapter.model.Chapter
|
||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
import tachiyomi.domain.source.service.SourceManager
|
import tachiyomi.domain.source.service.SourceManager
|
||||||
|
|
||||||
|
@Inject
|
||||||
class DeleteDownload(
|
class DeleteDownload(
|
||||||
private val sourceManager: SourceManager,
|
private val sourceManager: SourceManager,
|
||||||
private val downloadManager: DownloadManager,
|
private val downloadManager: DownloadManager,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
suspend fun awaitAll(manga: Manga, vararg chapters: Chapter) = withNonCancellableContext {
|
suspend fun awaitAll(manga: Manga, vararg chapters: Chapter) {
|
||||||
sourceManager.get(manga.source)?.let { source ->
|
sourceManager.get(manga.source)?.let { source ->
|
||||||
downloadManager.deleteChapters(chapters.toList(), manga, source)
|
downloadManager.deleteChapters(chapters.toList(), manga, source)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +1,25 @@
|
|||||||
package eu.kanade.domain.extension.interactor
|
package eu.kanade.domain.extension.interactor
|
||||||
|
|
||||||
|
import dev.zacsweers.metro.Inject
|
||||||
import eu.kanade.domain.source.service.SourcePreferences
|
import eu.kanade.domain.source.service.SourcePreferences
|
||||||
import eu.kanade.tachiyomi.extension.ExtensionManager
|
import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
|
|
||||||
|
@Inject
|
||||||
class GetExtensionLanguages(
|
class GetExtensionLanguages(
|
||||||
private val preferences: SourcePreferences,
|
private val preferences: SourcePreferences,
|
||||||
private val extensionManager: ExtensionManager,
|
private val extensionManager: ExtensionManager,
|
||||||
) {
|
) {
|
||||||
fun subscribe(): Flow<List<String>> {
|
fun subscribe(): Flow<List<String>> {
|
||||||
return combine(
|
return combine(
|
||||||
preferences.enabledLanguages().changes(),
|
preferences.enabledLanguages.changes(),
|
||||||
extensionManager.availableExtensionsFlow,
|
extensionManager.availableExtensionsFlow,
|
||||||
) { enabledLanguage, availableExtensions ->
|
) { enabledLanguage, availableExtensions ->
|
||||||
availableExtensions
|
availableExtensions
|
||||||
.flatMap { ext ->
|
.flatMap { ext ->
|
||||||
if (ext.sources.isEmpty()) {
|
ext.sources.map { it.lang }
|
||||||
listOf(ext.lang)
|
|
||||||
} else {
|
|
||||||
ext.sources.map { it.lang }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.distinct()
|
.distinct()
|
||||||
.sortedWith(
|
.sortedWith(
|
||||||
|
|||||||
@@ -1,21 +1,23 @@
|
|||||||
package eu.kanade.domain.extension.interactor
|
package eu.kanade.domain.extension.interactor
|
||||||
|
|
||||||
|
import dev.zacsweers.metro.Inject
|
||||||
import eu.kanade.domain.source.service.SourcePreferences
|
import eu.kanade.domain.source.service.SourcePreferences
|
||||||
import eu.kanade.tachiyomi.extension.model.Extension
|
import eu.kanade.tachiyomi.extension.model.Extension
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
|
||||||
|
@Inject
|
||||||
class GetExtensionSources(
|
class GetExtensionSources(
|
||||||
private val preferences: SourcePreferences,
|
private val preferences: SourcePreferences,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun subscribe(extension: Extension.Installed): Flow<List<ExtensionSourceItem>> {
|
fun subscribe(extension: Extension.Loaded): Flow<List<ExtensionSourceItem>> {
|
||||||
val isMultiSource = extension.sources.size > 1
|
val isMultiSource = extension.sources.size > 1
|
||||||
val isMultiLangSingleSource =
|
val isMultiLangSingleSource =
|
||||||
isMultiSource && extension.sources.map { it.name }.distinct().size == 1
|
isMultiSource && extension.sources.map { it.name }.distinct().size == 1
|
||||||
|
|
||||||
return preferences.disabledSources().changes().map { disabledSources ->
|
return preferences.disabledSources.changes().map { disabledSources ->
|
||||||
fun Source.isEnabled() = id.toString() !in disabledSources
|
fun Source.isEnabled() = id.toString() !in disabledSources
|
||||||
|
|
||||||
extension.sources
|
extension.sources
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package eu.kanade.domain.extension.interactor
|
package eu.kanade.domain.extension.interactor
|
||||||
|
|
||||||
|
import dev.zacsweers.metro.Inject
|
||||||
import eu.kanade.domain.extension.model.Extensions
|
import eu.kanade.domain.extension.model.Extensions
|
||||||
import eu.kanade.domain.source.service.SourcePreferences
|
import eu.kanade.domain.source.service.SourcePreferences
|
||||||
import eu.kanade.tachiyomi.extension.ExtensionManager
|
import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||||
@@ -7,41 +8,35 @@ import eu.kanade.tachiyomi.extension.model.Extension
|
|||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
|
|
||||||
|
@Inject
|
||||||
class GetExtensionsByType(
|
class GetExtensionsByType(
|
||||||
private val preferences: SourcePreferences,
|
private val preferences: SourcePreferences,
|
||||||
private val extensionManager: ExtensionManager,
|
private val extensionManager: ExtensionManager,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun subscribe(): Flow<Extensions> {
|
fun subscribe(): Flow<Extensions> {
|
||||||
val showNsfwSources = preferences.showNsfwSource().get()
|
val enabledContentWarnings = preferences.enabledContentWarnings.get()
|
||||||
|
|
||||||
return combine(
|
return combine(
|
||||||
preferences.enabledLanguages().changes(),
|
preferences.enabledLanguages.changes(),
|
||||||
extensionManager.installedExtensionsFlow,
|
extensionManager.loadedExtensionsFlow,
|
||||||
extensionManager.untrustedExtensionsFlow,
|
extensionManager.notLoadedExtensionsFlow,
|
||||||
extensionManager.availableExtensionsFlow,
|
extensionManager.availableExtensionsFlow,
|
||||||
) { enabledLanguages, _installed, _untrusted, _available ->
|
) { enabledLanguages, _loaded, _notLoaded, _available ->
|
||||||
val (updates, installed) = _installed
|
val byName = compareBy<Extension, String>(String.CASE_INSENSITIVE_ORDER) { it.name }
|
||||||
.filter { (showNsfwSources || !it.isNsfw) }
|
|
||||||
.sortedWith(
|
|
||||||
compareBy<Extension.Installed> { !it.isObsolete }
|
|
||||||
.thenBy(String.CASE_INSENSITIVE_ORDER) { it.name },
|
|
||||||
)
|
|
||||||
.partition { it.hasUpdate }
|
|
||||||
|
|
||||||
val untrusted = _untrusted
|
val (loadedUpdates, loaded) = _loaded.partition { it.hasUpdate }
|
||||||
.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name })
|
val (notLoadedUpdates, notLoaded) = _notLoaded.partition { it.hasUpdate }
|
||||||
|
|
||||||
|
val updates = (loadedUpdates + notLoadedUpdates).sortedWith(byName)
|
||||||
|
|
||||||
|
val installedSignatures = (_loaded + _notLoaded).groupBy({ it.pkgName }, { it.signatures })
|
||||||
val available = _available
|
val available = _available
|
||||||
.filter { extension ->
|
.filter { extension ->
|
||||||
_installed.none { it.pkgName == extension.pkgName } &&
|
installedSignatures[extension.pkgName].orEmpty().none { extension.store.signingKey in it } &&
|
||||||
_untrusted.none { it.pkgName == extension.pkgName } &&
|
extension.contentWarning in enabledContentWarnings
|
||||||
(showNsfwSources || !extension.isNsfw)
|
|
||||||
}
|
}
|
||||||
.flatMap { ext ->
|
.flatMap { ext ->
|
||||||
if (ext.sources.isEmpty()) {
|
|
||||||
return@flatMap if (ext.lang in enabledLanguages) listOf(ext) else emptyList()
|
|
||||||
}
|
|
||||||
ext.sources.filter { it.lang in enabledLanguages }
|
ext.sources.filter { it.lang in enabledLanguages }
|
||||||
.map {
|
.map {
|
||||||
ext.copy(
|
ext.copy(
|
||||||
@@ -52,9 +47,17 @@ class GetExtensionsByType(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name })
|
.sortedWith(
|
||||||
|
compareBy<Extension.Available, String>(String.CASE_INSENSITIVE_ORDER) { it.name }
|
||||||
|
.thenBy { it.store.signingKey },
|
||||||
|
)
|
||||||
|
|
||||||
Extensions(updates, installed, available, untrusted)
|
Extensions(
|
||||||
|
updates = updates,
|
||||||
|
loaded = loaded.sortedWith(compareBy<Extension.Loaded> { !it.isObsolete }.then(byName)),
|
||||||
|
available = available,
|
||||||
|
notLoaded = notLoaded.sortedWith(byName),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,23 +2,31 @@ package eu.kanade.domain.extension.interactor
|
|||||||
|
|
||||||
import android.content.pm.PackageInfo
|
import android.content.pm.PackageInfo
|
||||||
import androidx.core.content.pm.PackageInfoCompat
|
import androidx.core.content.pm.PackageInfoCompat
|
||||||
|
import dev.zacsweers.metro.Inject
|
||||||
import eu.kanade.domain.source.service.SourcePreferences
|
import eu.kanade.domain.source.service.SourcePreferences
|
||||||
import mihon.domain.extensionrepo.repository.ExtensionRepoRepository
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
import kotlinx.coroutines.flow.drop
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.merge
|
||||||
|
import mihon.domain.extension.model.ExtensionStore
|
||||||
|
import mihon.domain.extension.repository.ExtensionStoreRepository
|
||||||
import tachiyomi.core.common.preference.getAndSet
|
import tachiyomi.core.common.preference.getAndSet
|
||||||
|
|
||||||
|
@Inject
|
||||||
class TrustExtension(
|
class TrustExtension(
|
||||||
private val extensionRepoRepository: ExtensionRepoRepository,
|
private val repository: ExtensionStoreRepository,
|
||||||
private val preferences: SourcePreferences,
|
private val preferences: SourcePreferences,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
suspend fun isTrusted(pkgInfo: PackageInfo, fingerprints: List<String>): Boolean {
|
fun isTrusted(pkgInfo: PackageInfo, fingerprints: List<String>, stores: List<ExtensionStore>): Boolean {
|
||||||
val trustedFingerprints = extensionRepoRepository.getAll().map { it.signingKeyFingerprint }.toHashSet()
|
val trustedFingerprints = stores.mapTo(HashSet()) { it.signingKey }
|
||||||
val key = "${pkgInfo.packageName}:${PackageInfoCompat.getLongVersionCode(pkgInfo)}:${fingerprints.last()}"
|
val key = "${pkgInfo.packageName}:${PackageInfoCompat.getLongVersionCode(pkgInfo)}:${fingerprints.last()}"
|
||||||
return trustedFingerprints.any { fingerprints.contains(it) } || key in preferences.trustedExtensions().get()
|
return trustedFingerprints.any { fingerprints.contains(it) } || key in preferences.trustedExtensions.get()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun trust(pkgName: String, versionCode: Long, signatureHash: String) {
|
fun trust(pkgName: String, versionCode: Long, signatureHash: String) {
|
||||||
preferences.trustedExtensions().getAndSet { exts ->
|
preferences.trustedExtensions.getAndSet { exts ->
|
||||||
// Remove previously trusted versions
|
// Remove previously trusted versions
|
||||||
val removed = exts.filterNot { it.startsWith("$pkgName:") }.toMutableSet()
|
val removed = exts.filterNot { it.startsWith("$pkgName:") }.toMutableSet()
|
||||||
|
|
||||||
@@ -27,6 +35,25 @@ class TrustExtension(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun revokeAll() {
|
fun revokeAll() {
|
||||||
preferences.trustedExtensions().delete()
|
preferences.trustedExtensions.delete()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emits whenever what counts as trusted changes, either because a store was added or removed or
|
||||||
|
* because an extension was trusted or had its trust revoked. Both sources replay their current
|
||||||
|
* value, which is dropped.
|
||||||
|
*/
|
||||||
|
fun changes(): Flow<Unit> {
|
||||||
|
return merge(
|
||||||
|
// Stores are rewritten whenever their index is refreshed, so only their keys matter here
|
||||||
|
repository.getAllAsFlow()
|
||||||
|
.map { stores -> stores.mapTo(HashSet()) { it.signingKey } }
|
||||||
|
.distinctUntilChanged()
|
||||||
|
.drop(1),
|
||||||
|
preferences.trustedExtensions.changes()
|
||||||
|
.distinctUntilChanged()
|
||||||
|
.drop(1),
|
||||||
|
)
|
||||||
|
.map {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import eu.kanade.tachiyomi.extension.model.Extension
|
|||||||
|
|
||||||
data class Extensions(
|
data class Extensions(
|
||||||
val updates: List<Extension.Installed>,
|
val updates: List<Extension.Installed>,
|
||||||
val installed: List<Extension.Installed>,
|
val loaded: List<Extension.Loaded>,
|
||||||
val available: List<Extension.Available>,
|
val available: List<Extension.Available>,
|
||||||
val untrusted: List<Extension.Untrusted>,
|
val notLoaded: List<Extension.NotLoaded>,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,24 +1,19 @@
|
|||||||
package eu.kanade.domain.manga.interactor
|
package eu.kanade.domain.manga.interactor
|
||||||
|
|
||||||
|
import dev.zacsweers.metro.Inject
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.map
|
import tachiyomi.domain.manga.repository.MangaRepository
|
||||||
import tachiyomi.data.DatabaseHandler
|
|
||||||
|
|
||||||
|
@Inject
|
||||||
class GetExcludedScanlators(
|
class GetExcludedScanlators(
|
||||||
private val handler: DatabaseHandler,
|
private val mangaRepository: MangaRepository,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
suspend fun await(mangaId: Long): Set<String> {
|
suspend fun await(mangaId: Long): Set<String> {
|
||||||
return handler.awaitList {
|
return mangaRepository.getExcludedScanlators(mangaId)
|
||||||
excluded_scanlatorsQueries.getExcludedScanlatorsByMangaId(mangaId)
|
|
||||||
}
|
|
||||||
.toSet()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun subscribe(mangaId: Long): Flow<Set<String>> {
|
fun subscribe(mangaId: Long): Flow<Set<String>> {
|
||||||
return handler.subscribeToList {
|
return mangaRepository.getExcludedScanlatorsAsFlow(mangaId)
|
||||||
excluded_scanlatorsQueries.getExcludedScanlatorsByMangaId(mangaId)
|
|
||||||
}
|
|
||||||
.map { it.toSet() }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,14 @@
|
|||||||
package eu.kanade.domain.manga.interactor
|
package eu.kanade.domain.manga.interactor
|
||||||
|
|
||||||
import tachiyomi.data.DatabaseHandler
|
import dev.zacsweers.metro.Inject
|
||||||
|
import tachiyomi.domain.manga.repository.MangaRepository
|
||||||
|
|
||||||
|
@Inject
|
||||||
class SetExcludedScanlators(
|
class SetExcludedScanlators(
|
||||||
private val handler: DatabaseHandler,
|
private val mangaRepository: MangaRepository,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
suspend fun await(mangaId: Long, excludedScanlators: Set<String>) {
|
suspend fun await(mangaId: Long, excludedScanlators: Set<String>) {
|
||||||
handler.await(inTransaction = true) {
|
mangaRepository.setExcludedScanlators(mangaId, excludedScanlators)
|
||||||
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,10 +1,12 @@
|
|||||||
package eu.kanade.domain.manga.interactor
|
package eu.kanade.domain.manga.interactor
|
||||||
|
|
||||||
|
import dev.zacsweers.metro.Inject
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation
|
import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode
|
import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode
|
||||||
import tachiyomi.domain.manga.model.MangaUpdate
|
import tachiyomi.domain.manga.model.MangaUpdate
|
||||||
import tachiyomi.domain.manga.repository.MangaRepository
|
import tachiyomi.domain.manga.repository.MangaRepository
|
||||||
|
|
||||||
|
@Inject
|
||||||
class SetMangaViewerFlags(
|
class SetMangaViewerFlags(
|
||||||
private val mangaRepository: MangaRepository,
|
private val mangaRepository: MangaRepository,
|
||||||
) {
|
) {
|
||||||
@@ -12,20 +14,18 @@ class SetMangaViewerFlags(
|
|||||||
suspend fun awaitSetReadingMode(id: Long, flag: Long) {
|
suspend fun awaitSetReadingMode(id: Long, flag: Long) {
|
||||||
val manga = mangaRepository.getMangaById(id)
|
val manga = mangaRepository.getMangaById(id)
|
||||||
mangaRepository.update(
|
mangaRepository.update(
|
||||||
MangaUpdate(
|
MangaUpdate(id) {
|
||||||
id = id,
|
viewerFlags = manga.viewerFlags.setFlag(flag, ReadingMode.MASK.toLong())
|
||||||
viewerFlags = manga.viewerFlags.setFlag(flag, ReadingMode.MASK.toLong()),
|
},
|
||||||
),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun awaitSetOrientation(id: Long, flag: Long) {
|
suspend fun awaitSetOrientation(id: Long, flag: Long) {
|
||||||
val manga = mangaRepository.getMangaById(id)
|
val manga = mangaRepository.getMangaById(id)
|
||||||
mangaRepository.update(
|
mangaRepository.update(
|
||||||
MangaUpdate(
|
MangaUpdate(id) {
|
||||||
id = id,
|
viewerFlags = manga.viewerFlags.setFlag(flag, ReaderOrientation.MASK.toLong())
|
||||||
viewerFlags = manga.viewerFlags.setFlag(flag, ReaderOrientation.MASK.toLong()),
|
},
|
||||||
),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,17 @@
|
|||||||
package eu.kanade.domain.manga.interactor
|
package eu.kanade.domain.manga.interactor
|
||||||
|
|
||||||
import eu.kanade.domain.manga.model.hasCustomCover
|
import dev.zacsweers.metro.Inject
|
||||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
import kotlinx.datetime.LocalDateTime
|
||||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
import kotlinx.datetime.TimeZone
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import kotlinx.datetime.toLocalDateTime
|
||||||
import tachiyomi.domain.library.service.LibraryPreferences
|
|
||||||
import tachiyomi.domain.manga.interactor.FetchInterval
|
import tachiyomi.domain.manga.interactor.FetchInterval
|
||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
import tachiyomi.domain.manga.model.MangaUpdate
|
import tachiyomi.domain.manga.model.MangaUpdate
|
||||||
import tachiyomi.domain.manga.repository.MangaRepository
|
import tachiyomi.domain.manga.repository.MangaRepository
|
||||||
import tachiyomi.source.local.isLocal
|
import kotlin.time.Clock
|
||||||
import uy.kohesive.injekt.Injekt
|
import kotlin.time.Instant
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
import java.time.Instant
|
|
||||||
import java.time.ZonedDateTime
|
|
||||||
|
|
||||||
|
@Inject
|
||||||
class UpdateManga(
|
class UpdateManga(
|
||||||
private val mangaRepository: MangaRepository,
|
private val mangaRepository: MangaRepository,
|
||||||
private val fetchInterval: FetchInterval,
|
private val fetchInterval: FetchInterval,
|
||||||
@@ -28,92 +25,47 @@ class UpdateManga(
|
|||||||
return mangaRepository.updateAll(mangaUpdates)
|
return mangaRepository.updateAll(mangaUpdates)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun awaitUpdateFromSource(
|
|
||||||
localManga: Manga,
|
|
||||||
remoteManga: SManga,
|
|
||||||
manualFetch: Boolean,
|
|
||||||
coverCache: CoverCache = Injekt.get(),
|
|
||||||
libraryPreferences: LibraryPreferences = Injekt.get(),
|
|
||||||
downloadManager: DownloadManager = Injekt.get(),
|
|
||||||
): Boolean {
|
|
||||||
val remoteTitle = try {
|
|
||||||
remoteManga.title
|
|
||||||
} catch (_: UninitializedPropertyAccessException) {
|
|
||||||
""
|
|
||||||
}
|
|
||||||
|
|
||||||
// if the manga isn't a favorite (or 'update titles' preference is enabled), set its title from source and update in db
|
|
||||||
val title =
|
|
||||||
if (remoteTitle.isNotEmpty() && (!localManga.favorite || libraryPreferences.updateMangaTitles().get())) {
|
|
||||||
remoteTitle
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
val coverLastModified =
|
|
||||||
when {
|
|
||||||
// Never refresh covers if the url is empty to avoid "losing" existing covers
|
|
||||||
remoteManga.thumbnail_url.isNullOrEmpty() -> null
|
|
||||||
!manualFetch && localManga.thumbnailUrl == remoteManga.thumbnail_url -> null
|
|
||||||
localManga.isLocal() -> Instant.now().toEpochMilli()
|
|
||||||
localManga.hasCustomCover(coverCache) -> {
|
|
||||||
coverCache.deleteFromCache(localManga, false)
|
|
||||||
null
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
coverCache.deleteFromCache(localManga, false)
|
|
||||||
Instant.now().toEpochMilli()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val thumbnailUrl = remoteManga.thumbnail_url?.takeIf { it.isNotEmpty() }
|
|
||||||
|
|
||||||
val success = mangaRepository.update(
|
|
||||||
MangaUpdate(
|
|
||||||
id = localManga.id,
|
|
||||||
title = title,
|
|
||||||
coverLastModified = coverLastModified,
|
|
||||||
author = remoteManga.author,
|
|
||||||
artist = remoteManga.artist,
|
|
||||||
description = remoteManga.description,
|
|
||||||
genre = remoteManga.getGenres(),
|
|
||||||
thumbnailUrl = thumbnailUrl,
|
|
||||||
status = remoteManga.status.toLong(),
|
|
||||||
updateStrategy = remoteManga.update_strategy,
|
|
||||||
initialized = true,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
if (success && title != null) {
|
|
||||||
downloadManager.renameManga(localManga, title)
|
|
||||||
}
|
|
||||||
return success
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun awaitUpdateFetchInterval(
|
suspend fun awaitUpdateFetchInterval(
|
||||||
manga: Manga,
|
manga: Manga,
|
||||||
dateTime: ZonedDateTime = ZonedDateTime.now(),
|
timeZone: TimeZone = TimeZone.currentSystemDefault(),
|
||||||
window: Pair<Long, Long> = fetchInterval.getWindow(dateTime),
|
dateTime: LocalDateTime = Clock.System.now().toLocalDateTime(timeZone),
|
||||||
|
window: ClosedRange<Instant>? = fetchInterval.getWindow(dateTime.date, timeZone),
|
||||||
): Boolean {
|
): Boolean {
|
||||||
return mangaRepository.update(
|
return mangaRepository.update(
|
||||||
fetchInterval.toMangaUpdate(manga, dateTime, window),
|
fetchInterval.withFetchInterval(manga, dateTime, timeZone, window),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun awaitUpdateLastUpdate(mangaId: Long): Boolean {
|
/**
|
||||||
return mangaRepository.update(MangaUpdate(id = mangaId, lastUpdate = Instant.now().toEpochMilli()))
|
* Updates the fetch interval and marks the chapter list as changed in a single write.
|
||||||
|
*/
|
||||||
|
suspend fun awaitUpdateFetchIntervalAndLastUpdate(
|
||||||
|
manga: Manga,
|
||||||
|
timeZone: TimeZone,
|
||||||
|
dateTime: LocalDateTime,
|
||||||
|
window: ClosedRange<Instant>?,
|
||||||
|
foundRelease: Boolean,
|
||||||
|
): Boolean {
|
||||||
|
return mangaRepository.update(
|
||||||
|
fetchInterval.withFetchInterval(manga, dateTime, timeZone, window, foundRelease).apply {
|
||||||
|
lastUpdate = Clock.System.now()
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun awaitUpdateCoverLastModified(mangaId: Long): Boolean {
|
suspend fun awaitUpdateCoverLastModified(mangaId: Long): Boolean {
|
||||||
return mangaRepository.update(MangaUpdate(id = mangaId, coverLastModified = Instant.now().toEpochMilli()))
|
return mangaRepository.update(
|
||||||
|
MangaUpdate(mangaId) {
|
||||||
|
coverLastModified = Clock.System.now()
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun awaitUpdateFavorite(mangaId: Long, favorite: Boolean): Boolean {
|
suspend fun awaitUpdateFavorite(mangaId: Long, favorite: Boolean): Boolean {
|
||||||
val dateAdded = when (favorite) {
|
val update = when (favorite) {
|
||||||
true -> Instant.now().toEpochMilli()
|
true -> MangaUpdate(mangaId) { favoriteAt = Clock.System.now() }
|
||||||
false -> 0
|
false -> MangaUpdate(mangaId) { favoriteAt = null }
|
||||||
}
|
}
|
||||||
return mangaRepository.update(
|
return mangaRepository.update(update)
|
||||||
MangaUpdate(id = mangaId, favorite = favorite, dateAdded = dateAdded),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
package eu.kanade.domain.manga.model
|
package eu.kanade.domain.manga.model
|
||||||
|
|
||||||
import eu.kanade.domain.base.BasePreferences
|
import android.content.Context
|
||||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation
|
import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode
|
import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode
|
||||||
|
import kotlinx.datetime.TimeZone
|
||||||
|
import kotlinx.datetime.number
|
||||||
|
import kotlinx.datetime.toLocalDateTime
|
||||||
|
import mihon.app.di.appGraph
|
||||||
import tachiyomi.core.common.preference.TriState
|
import tachiyomi.core.common.preference.TriState
|
||||||
import tachiyomi.core.metadata.comicinfo.ComicInfo
|
import tachiyomi.core.metadata.comicinfo.ComicInfo
|
||||||
import tachiyomi.core.metadata.comicinfo.ComicInfoPublishingStatus
|
import tachiyomi.core.metadata.comicinfo.ComicInfoPublishingStatus
|
||||||
@@ -22,7 +26,7 @@ val Manga.readerOrientation: Long
|
|||||||
|
|
||||||
val Manga.downloadedFilter: TriState
|
val Manga.downloadedFilter: TriState
|
||||||
get() {
|
get() {
|
||||||
if (Injekt.get<BasePreferences>().downloadedOnly().get()) return TriState.ENABLED_IS
|
if (Injekt.get<Context>().appGraph.basePreferences.downloadedOnly.get()) return TriState.ENABLED_IS
|
||||||
return when (downloadedFilterRaw) {
|
return when (downloadedFilterRaw) {
|
||||||
Manga.CHAPTER_SHOW_DOWNLOADED -> TriState.ENABLED_IS
|
Manga.CHAPTER_SHOW_DOWNLOADED -> TriState.ENABLED_IS
|
||||||
Manga.CHAPTER_SHOW_NOT_DOWNLOADED -> TriState.ENABLED_NOT
|
Manga.CHAPTER_SHOW_NOT_DOWNLOADED -> TriState.ENABLED_NOT
|
||||||
@@ -45,6 +49,7 @@ fun Manga.toSManga(): SManga = SManga.create().also {
|
|||||||
it.status = status.toInt()
|
it.status = status.toInt()
|
||||||
it.thumbnail_url = thumbnailUrl
|
it.thumbnail_url = thumbnailUrl
|
||||||
it.initialized = initialized
|
it.initialized = initialized
|
||||||
|
it.memo = memo
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Manga.copyFrom(other: SManga): Manga {
|
fun Manga.copyFrom(other: SManga): Manga {
|
||||||
@@ -66,11 +71,12 @@ fun Manga.copyFrom(other: SManga): Manga {
|
|||||||
status = other.status.toLong(),
|
status = other.status.toLong(),
|
||||||
updateStrategy = other.update_strategy,
|
updateStrategy = other.update_strategy,
|
||||||
initialized = other.initialized && initialized,
|
initialized = other.initialized && initialized,
|
||||||
|
memo = other.memo,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Manga.hasCustomCover(coverCache: CoverCache = Injekt.get()): Boolean {
|
fun Manga.hasCustomCover(coverCache: CoverCache = Injekt.get<Context>().appGraph.coverCache): Boolean {
|
||||||
return coverCache.getCustomCoverFile(id).exists()
|
return coverCache.hasCustomCover(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -82,30 +88,37 @@ fun getComicInfo(
|
|||||||
urls: List<String>,
|
urls: List<String>,
|
||||||
categories: List<String>?,
|
categories: List<String>?,
|
||||||
sourceName: String,
|
sourceName: String,
|
||||||
) = ComicInfo(
|
): ComicInfo {
|
||||||
title = ComicInfo.Title(chapter.name),
|
val date = chapter.dateUpload?.toLocalDateTime(TimeZone.currentSystemDefault())
|
||||||
series = ComicInfo.Series(manga.title),
|
|
||||||
number = chapter.chapterNumber.takeIf { it >= 0 }?.let {
|
return ComicInfo(
|
||||||
if ((it.rem(1) == 0.0)) {
|
title = ComicInfo.Title(chapter.name),
|
||||||
ComicInfo.Number(it.toInt().toString())
|
series = ComicInfo.Series(manga.title),
|
||||||
} else {
|
number = chapter.chapterNumber.takeIf { it >= 0 }?.let {
|
||||||
ComicInfo.Number(it.toString())
|
if ((it.rem(1) == 0.0)) {
|
||||||
}
|
ComicInfo.Number(it.toInt().toString())
|
||||||
},
|
} else {
|
||||||
web = ComicInfo.Web(urls.joinToString(" ")),
|
ComicInfo.Number(it.toString())
|
||||||
summary = manga.description?.let { ComicInfo.Summary(it) },
|
}
|
||||||
writer = manga.author?.let { ComicInfo.Writer(it) },
|
},
|
||||||
penciller = manga.artist?.let { ComicInfo.Penciller(it) },
|
web = ComicInfo.Web(urls.joinToString(" ")),
|
||||||
translator = chapter.scanlator?.let { ComicInfo.Translator(it) },
|
summary = manga.description?.let { ComicInfo.Summary(it) },
|
||||||
genre = manga.genre?.let { ComicInfo.Genre(it.joinToString()) },
|
writer = manga.author?.let { ComicInfo.Writer(it) },
|
||||||
publishingStatus = ComicInfo.PublishingStatusTachiyomi(
|
penciller = manga.artist?.let { ComicInfo.Penciller(it) },
|
||||||
ComicInfoPublishingStatus.toComicInfoValue(manga.status),
|
translator = chapter.scanlator?.let { ComicInfo.Translator(it) },
|
||||||
),
|
genre = manga.genre?.let { ComicInfo.Genre(it.joinToString()) },
|
||||||
categories = categories?.let { ComicInfo.CategoriesTachiyomi(it.joinToString()) },
|
publishingStatus = ComicInfo.PublishingStatusTachiyomi(
|
||||||
source = ComicInfo.SourceMihon(sourceName),
|
ComicInfoPublishingStatus.toComicInfoValue(manga.status),
|
||||||
inker = null,
|
),
|
||||||
colorist = null,
|
categories = categories?.let { ComicInfo.CategoriesTachiyomi(it.joinToString()) },
|
||||||
letterer = null,
|
source = ComicInfo.SourceMihon(sourceName),
|
||||||
coverArtist = null,
|
inker = null,
|
||||||
tags = null,
|
colorist = null,
|
||||||
)
|
letterer = null,
|
||||||
|
coverArtist = null,
|
||||||
|
tags = null,
|
||||||
|
year = date?.year?.let { ComicInfo.Year(it) },
|
||||||
|
month = date?.month?.number?.let { ComicInfo.Month(it) },
|
||||||
|
day = date?.day?.let { ComicInfo.Day(it) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package eu.kanade.domain.source.interactor
|
package eu.kanade.domain.source.interactor
|
||||||
|
|
||||||
|
import dev.zacsweers.metro.Inject
|
||||||
import eu.kanade.domain.source.service.SourcePreferences
|
import eu.kanade.domain.source.service.SourcePreferences
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
@@ -10,6 +11,7 @@ import tachiyomi.domain.source.model.Source
|
|||||||
import tachiyomi.domain.source.repository.SourceRepository
|
import tachiyomi.domain.source.repository.SourceRepository
|
||||||
import tachiyomi.source.local.isLocal
|
import tachiyomi.source.local.isLocal
|
||||||
|
|
||||||
|
@Inject
|
||||||
class GetEnabledSources(
|
class GetEnabledSources(
|
||||||
private val repository: SourceRepository,
|
private val repository: SourceRepository,
|
||||||
private val preferences: SourcePreferences,
|
private val preferences: SourcePreferences,
|
||||||
@@ -17,10 +19,10 @@ class GetEnabledSources(
|
|||||||
|
|
||||||
fun subscribe(): Flow<List<Source>> {
|
fun subscribe(): Flow<List<Source>> {
|
||||||
return combine(
|
return combine(
|
||||||
preferences.pinnedSources().changes(),
|
preferences.pinnedSources.changes(),
|
||||||
preferences.enabledLanguages().changes(),
|
preferences.enabledLanguages.changes(),
|
||||||
preferences.disabledSources().changes(),
|
preferences.disabledSources.changes(),
|
||||||
preferences.lastUsedSource().changes(),
|
preferences.lastUsedSource.changes(),
|
||||||
repository.getSources(),
|
repository.getSources(),
|
||||||
) { pinnedSourceIds, enabledLanguages, disabledSources, lastUsedSource, sources ->
|
) { pinnedSourceIds, enabledLanguages, disabledSources, lastUsedSource, sources ->
|
||||||
sources
|
sources
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package eu.kanade.domain.source.interactor
|
package eu.kanade.domain.source.interactor
|
||||||
|
|
||||||
|
import dev.zacsweers.metro.Inject
|
||||||
import eu.kanade.domain.base.BasePreferences
|
import eu.kanade.domain.base.BasePreferences
|
||||||
import eu.kanade.domain.source.service.SourcePreferences
|
import eu.kanade.domain.source.service.SourcePreferences
|
||||||
import eu.kanade.tachiyomi.extension.ExtensionManager
|
import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||||
@@ -7,25 +8,26 @@ import kotlinx.coroutines.flow.Flow
|
|||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
|
||||||
|
@Inject
|
||||||
class GetIncognitoState(
|
class GetIncognitoState(
|
||||||
private val basePreferences: BasePreferences,
|
private val basePreferences: BasePreferences,
|
||||||
private val sourcePreferences: SourcePreferences,
|
private val sourcePreferences: SourcePreferences,
|
||||||
private val extensionManager: ExtensionManager,
|
private val extensionManager: ExtensionManager,
|
||||||
) {
|
) {
|
||||||
fun await(sourceId: Long?): Boolean {
|
suspend fun await(sourceId: Long?): Boolean {
|
||||||
if (basePreferences.incognitoMode().get()) return true
|
if (basePreferences.incognitoMode.get()) return true
|
||||||
if (sourceId == null) return false
|
if (sourceId == null) return false
|
||||||
val extensionPackage = extensionManager.getExtensionPackage(sourceId) ?: return false
|
val extensionPackage = extensionManager.getExtensionPackage(sourceId) ?: return false
|
||||||
|
|
||||||
return extensionPackage in sourcePreferences.incognitoExtensions().get()
|
return extensionPackage in sourcePreferences.incognitoExtensions.get()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun subscribe(sourceId: Long?): Flow<Boolean> {
|
fun subscribe(sourceId: Long?): Flow<Boolean> {
|
||||||
if (sourceId == null) return basePreferences.incognitoMode().changes()
|
if (sourceId == null) return basePreferences.incognitoMode.changes()
|
||||||
|
|
||||||
return combine(
|
return combine(
|
||||||
basePreferences.incognitoMode().changes(),
|
basePreferences.incognitoMode.changes(),
|
||||||
sourcePreferences.incognitoExtensions().changes(),
|
sourcePreferences.incognitoExtensions.changes(),
|
||||||
extensionManager.getExtensionPackageAsFlow(sourceId),
|
extensionManager.getExtensionPackageAsFlow(sourceId),
|
||||||
) { incognito, incognitoExtensions, extensionPackage ->
|
) { incognito, incognitoExtensions, extensionPackage ->
|
||||||
incognito || (extensionPackage in incognitoExtensions)
|
incognito || (extensionPackage in incognitoExtensions)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package eu.kanade.domain.source.interactor
|
package eu.kanade.domain.source.interactor
|
||||||
|
|
||||||
|
import dev.zacsweers.metro.Inject
|
||||||
import eu.kanade.domain.source.service.SourcePreferences
|
import eu.kanade.domain.source.service.SourcePreferences
|
||||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
@@ -8,6 +9,7 @@ import tachiyomi.domain.source.model.Source
|
|||||||
import tachiyomi.domain.source.repository.SourceRepository
|
import tachiyomi.domain.source.repository.SourceRepository
|
||||||
import java.util.SortedMap
|
import java.util.SortedMap
|
||||||
|
|
||||||
|
@Inject
|
||||||
class GetLanguagesWithSources(
|
class GetLanguagesWithSources(
|
||||||
private val repository: SourceRepository,
|
private val repository: SourceRepository,
|
||||||
private val preferences: SourcePreferences,
|
private val preferences: SourcePreferences,
|
||||||
@@ -15,8 +17,8 @@ class GetLanguagesWithSources(
|
|||||||
|
|
||||||
fun subscribe(): Flow<SortedMap<String, List<Source>>> {
|
fun subscribe(): Flow<SortedMap<String, List<Source>>> {
|
||||||
return combine(
|
return combine(
|
||||||
preferences.enabledLanguages().changes(),
|
preferences.enabledLanguages.changes(),
|
||||||
preferences.disabledSources().changes(),
|
preferences.disabledSources.changes(),
|
||||||
repository.getOnlineSources(),
|
repository.getOnlineSources(),
|
||||||
) { enabledLanguage, disabledSource, onlineSources ->
|
) { enabledLanguage, disabledSource, onlineSources ->
|
||||||
val sortedSources = onlineSources.sortedWith(
|
val sortedSources = onlineSources.sortedWith(
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package eu.kanade.domain.source.interactor
|
package eu.kanade.domain.source.interactor
|
||||||
|
|
||||||
|
import dev.zacsweers.metro.Inject
|
||||||
import eu.kanade.domain.source.service.SourcePreferences
|
import eu.kanade.domain.source.service.SourcePreferences
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
@@ -9,6 +10,7 @@ import tachiyomi.domain.source.repository.SourceRepository
|
|||||||
import tachiyomi.source.local.isLocal
|
import tachiyomi.source.local.isLocal
|
||||||
import java.util.Collections
|
import java.util.Collections
|
||||||
|
|
||||||
|
@Inject
|
||||||
class GetSourcesWithFavoriteCount(
|
class GetSourcesWithFavoriteCount(
|
||||||
private val repository: SourceRepository,
|
private val repository: SourceRepository,
|
||||||
private val preferences: SourcePreferences,
|
private val preferences: SourcePreferences,
|
||||||
@@ -16,8 +18,8 @@ class GetSourcesWithFavoriteCount(
|
|||||||
|
|
||||||
fun subscribe(): Flow<List<Pair<Source, Long>>> {
|
fun subscribe(): Flow<List<Pair<Source, Long>>> {
|
||||||
return combine(
|
return combine(
|
||||||
preferences.migrationSortingDirection().changes(),
|
preferences.migrationSortingDirection.changes(),
|
||||||
preferences.migrationSortingMode().changes(),
|
preferences.migrationSortingMode.changes(),
|
||||||
repository.getSourcesWithFavoriteCount(),
|
repository.getSourcesWithFavoriteCount(),
|
||||||
) { direction, mode, list ->
|
) { direction, mode, list ->
|
||||||
list
|
list
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
package eu.kanade.domain.source.interactor
|
package eu.kanade.domain.source.interactor
|
||||||
|
|
||||||
|
import dev.zacsweers.metro.Inject
|
||||||
import eu.kanade.domain.source.service.SourcePreferences
|
import eu.kanade.domain.source.service.SourcePreferences
|
||||||
|
|
||||||
|
@Inject
|
||||||
class SetMigrateSorting(
|
class SetMigrateSorting(
|
||||||
private val preferences: SourcePreferences,
|
private val preferences: SourcePreferences,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun await(mode: Mode, direction: Direction) {
|
fun await(mode: Mode, direction: Direction) {
|
||||||
preferences.migrationSortingMode().set(mode)
|
preferences.migrationSortingMode.set(mode)
|
||||||
preferences.migrationSortingDirection().set(direction)
|
preferences.migrationSortingDirection.set(direction)
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class Mode {
|
enum class Mode {
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
package eu.kanade.domain.source.interactor
|
package eu.kanade.domain.source.interactor
|
||||||
|
|
||||||
|
import dev.zacsweers.metro.Inject
|
||||||
import eu.kanade.domain.source.service.SourcePreferences
|
import eu.kanade.domain.source.service.SourcePreferences
|
||||||
import tachiyomi.core.common.preference.getAndSet
|
import tachiyomi.core.common.preference.getAndSet
|
||||||
|
|
||||||
|
@Inject
|
||||||
class ToggleIncognito(
|
class ToggleIncognito(
|
||||||
private val preferences: SourcePreferences,
|
private val preferences: SourcePreferences,
|
||||||
) {
|
) {
|
||||||
fun await(extensions: String, enable: Boolean) {
|
fun await(extensions: String, enable: Boolean) {
|
||||||
preferences.incognitoExtensions().getAndSet {
|
preferences.incognitoExtensions.getAndSet {
|
||||||
if (enable) it.plus(extensions) else it.minus(extensions)
|
if (enable) it.plus(extensions) else it.minus(extensions)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
package eu.kanade.domain.source.interactor
|
package eu.kanade.domain.source.interactor
|
||||||
|
|
||||||
|
import dev.zacsweers.metro.Inject
|
||||||
import eu.kanade.domain.source.service.SourcePreferences
|
import eu.kanade.domain.source.service.SourcePreferences
|
||||||
import tachiyomi.core.common.preference.getAndSet
|
import tachiyomi.core.common.preference.getAndSet
|
||||||
|
|
||||||
|
@Inject
|
||||||
class ToggleLanguage(
|
class ToggleLanguage(
|
||||||
val preferences: SourcePreferences,
|
val preferences: SourcePreferences,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun await(language: String) {
|
fun await(language: String) {
|
||||||
val isEnabled = language in preferences.enabledLanguages().get()
|
val isEnabled = language in preferences.enabledLanguages.get()
|
||||||
preferences.enabledLanguages().getAndSet { enabled ->
|
preferences.enabledLanguages.getAndSet { enabled ->
|
||||||
if (isEnabled) enabled.minus(language) else enabled.plus(language)
|
if (isEnabled) enabled.minus(language) else enabled.plus(language)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
package eu.kanade.domain.source.interactor
|
package eu.kanade.domain.source.interactor
|
||||||
|
|
||||||
|
import dev.zacsweers.metro.Inject
|
||||||
import eu.kanade.domain.source.service.SourcePreferences
|
import eu.kanade.domain.source.service.SourcePreferences
|
||||||
import tachiyomi.core.common.preference.getAndSet
|
import tachiyomi.core.common.preference.getAndSet
|
||||||
import tachiyomi.domain.source.model.Source
|
import tachiyomi.domain.source.model.Source
|
||||||
|
|
||||||
|
@Inject
|
||||||
class ToggleSource(
|
class ToggleSource(
|
||||||
private val preferences: SourcePreferences,
|
private val preferences: SourcePreferences,
|
||||||
) {
|
) {
|
||||||
@@ -13,19 +15,19 @@ class ToggleSource(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun await(sourceId: Long, enable: Boolean = isEnabled(sourceId)) {
|
fun await(sourceId: Long, enable: Boolean = isEnabled(sourceId)) {
|
||||||
preferences.disabledSources().getAndSet { disabled ->
|
preferences.disabledSources.getAndSet { disabled ->
|
||||||
if (enable) disabled.minus("$sourceId") else disabled.plus("$sourceId")
|
if (enable) disabled.minus("$sourceId") else disabled.plus("$sourceId")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun await(sourceIds: List<Long>, enable: Boolean) {
|
fun await(sourceIds: List<Long>, enable: Boolean) {
|
||||||
val transformedSourceIds = sourceIds.map { it.toString() }
|
val transformedSourceIds = sourceIds.map { it.toString() }
|
||||||
preferences.disabledSources().getAndSet { disabled ->
|
preferences.disabledSources.getAndSet { disabled ->
|
||||||
if (enable) disabled.minus(transformedSourceIds) else disabled.plus(transformedSourceIds)
|
if (enable) disabled.minus(transformedSourceIds) else disabled.plus(transformedSourceIds)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isEnabled(sourceId: Long): Boolean {
|
private fun isEnabled(sourceId: Long): Boolean {
|
||||||
return sourceId.toString() in preferences.disabledSources().get()
|
return sourceId.toString() in preferences.disabledSources.get()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
package eu.kanade.domain.source.interactor
|
package eu.kanade.domain.source.interactor
|
||||||
|
|
||||||
|
import dev.zacsweers.metro.Inject
|
||||||
import eu.kanade.domain.source.service.SourcePreferences
|
import eu.kanade.domain.source.service.SourcePreferences
|
||||||
import tachiyomi.core.common.preference.getAndSet
|
import tachiyomi.core.common.preference.getAndSet
|
||||||
import tachiyomi.domain.source.model.Source
|
import tachiyomi.domain.source.model.Source
|
||||||
|
|
||||||
|
@Inject
|
||||||
class ToggleSourcePin(
|
class ToggleSourcePin(
|
||||||
private val preferences: SourcePreferences,
|
private val preferences: SourcePreferences,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun await(source: Source) {
|
fun await(source: Source) {
|
||||||
val isPinned = source.id.toString() in preferences.pinnedSources().get()
|
val isPinned = source.id.toString() in preferences.pinnedSources.get()
|
||||||
preferences.pinnedSources().getAndSet { pinned ->
|
preferences.pinnedSources.getAndSet { pinned ->
|
||||||
if (isPinned) pinned.minus("${source.id}") else pinned.plus("${source.id}")
|
if (isPinned) pinned.minus("${source.id}") else pinned.plus("${source.id}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
package eu.kanade.domain.source.model
|
|
||||||
|
|
||||||
import androidx.compose.ui.graphics.ImageBitmap
|
|
||||||
import androidx.compose.ui.graphics.asImageBitmap
|
|
||||||
import androidx.core.graphics.drawable.toBitmap
|
|
||||||
import eu.kanade.tachiyomi.extension.ExtensionManager
|
|
||||||
import tachiyomi.domain.source.model.Source
|
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
|
|
||||||
val Source.icon: ImageBitmap?
|
|
||||||
get() {
|
|
||||||
return Injekt.get<ExtensionManager>().getAppIconForSource(id)
|
|
||||||
?.toBitmap()
|
|
||||||
?.asImageBitmap()
|
|
||||||
}
|
|
||||||
@@ -1,77 +1,104 @@
|
|||||||
package eu.kanade.domain.source.service
|
package eu.kanade.domain.source.service
|
||||||
|
|
||||||
|
import dev.zacsweers.metro.AppScope
|
||||||
|
import dev.zacsweers.metro.Inject
|
||||||
|
import dev.zacsweers.metro.SingleIn
|
||||||
import eu.kanade.domain.source.interactor.SetMigrateSorting
|
import eu.kanade.domain.source.interactor.SetMigrateSorting
|
||||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||||
|
import mihon.domain.extension.model.ContentWarning
|
||||||
import mihon.domain.migration.models.MigrationFlag
|
import mihon.domain.migration.models.MigrationFlag
|
||||||
import tachiyomi.core.common.preference.Preference
|
import tachiyomi.core.common.preference.Preference
|
||||||
import tachiyomi.core.common.preference.PreferenceStore
|
import tachiyomi.core.common.preference.PreferenceStore
|
||||||
import tachiyomi.core.common.preference.getEnum
|
import tachiyomi.core.common.preference.getEnum
|
||||||
|
import tachiyomi.core.common.preference.getEnumSet
|
||||||
import tachiyomi.core.common.preference.getLongArray
|
import tachiyomi.core.common.preference.getLongArray
|
||||||
import tachiyomi.domain.library.model.LibraryDisplayMode
|
import tachiyomi.domain.library.model.LibraryDisplayMode
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
@SingleIn(AppScope::class)
|
||||||
class SourcePreferences(
|
class SourcePreferences(
|
||||||
private val preferenceStore: PreferenceStore,
|
preferenceStore: PreferenceStore,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun sourceDisplayMode() = preferenceStore.getObjectFromString(
|
val sourceDisplayMode: Preference<LibraryDisplayMode> = preferenceStore.getObjectFromString(
|
||||||
"pref_display_mode_catalogue",
|
"pref_display_mode_catalogue",
|
||||||
LibraryDisplayMode.default,
|
LibraryDisplayMode.default,
|
||||||
LibraryDisplayMode.Serializer::serialize,
|
LibraryDisplayMode.Serializer::serialize,
|
||||||
LibraryDisplayMode.Serializer::deserialize,
|
LibraryDisplayMode.Serializer::deserialize,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun enabledLanguages() = preferenceStore.getStringSet("source_languages", LocaleHelper.getDefaultEnabledLanguages())
|
val enabledLanguages: Preference<Set<String>> = preferenceStore.getStringSet(
|
||||||
|
"source_languages",
|
||||||
|
LocaleHelper.getDefaultEnabledLanguages(),
|
||||||
|
)
|
||||||
|
|
||||||
fun disabledSources() = preferenceStore.getStringSet("hidden_catalogues", emptySet())
|
val disabledSources: Preference<Set<String>> = preferenceStore.getStringSet("hidden_catalogues", emptySet())
|
||||||
|
|
||||||
fun incognitoExtensions() = preferenceStore.getStringSet("incognito_extensions", emptySet())
|
val incognitoExtensions: Preference<Set<String>> = preferenceStore.getStringSet("incognito_extensions", emptySet())
|
||||||
|
|
||||||
fun pinnedSources() = preferenceStore.getStringSet("pinned_catalogues", emptySet())
|
val pinnedSources: Preference<Set<String>> = preferenceStore.getStringSet("pinned_catalogues", emptySet())
|
||||||
|
|
||||||
fun lastUsedSource() = preferenceStore.getLong(
|
val lastUsedSource: Preference<Long> = preferenceStore.getLong(
|
||||||
Preference.appStateKey("last_catalogue_source"),
|
Preference.appStateKey("last_catalogue_source"),
|
||||||
-1,
|
-1,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun showNsfwSource() = preferenceStore.getBoolean("show_nsfw_source", true)
|
val enabledContentWarnings: Preference<Set<ContentWarning>> = preferenceStore.getEnumSet(
|
||||||
|
"enabled_content_warnings",
|
||||||
|
setOf(ContentWarning.SAFE, ContentWarning.MIXED, ContentWarning.NSFW),
|
||||||
|
)
|
||||||
|
|
||||||
fun migrationSortingMode() = preferenceStore.getEnum("pref_migration_sorting", SetMigrateSorting.Mode.ALPHABETICAL)
|
val applyContentWarningsToInstalled: Preference<Boolean> = preferenceStore.getBoolean(
|
||||||
|
"apply_content_warnings_to_installed",
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
|
||||||
fun migrationSortingDirection() = preferenceStore.getEnum(
|
val migrationSortingMode: Preference<SetMigrateSorting.Mode> = preferenceStore.getEnum(
|
||||||
|
"pref_migration_sorting",
|
||||||
|
SetMigrateSorting.Mode.ALPHABETICAL,
|
||||||
|
)
|
||||||
|
|
||||||
|
val migrationSortingDirection: Preference<SetMigrateSorting.Direction> = preferenceStore.getEnum(
|
||||||
"pref_migration_direction",
|
"pref_migration_direction",
|
||||||
SetMigrateSorting.Direction.ASCENDING,
|
SetMigrateSorting.Direction.ASCENDING,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun hideInLibraryItems() = preferenceStore.getBoolean("browse_hide_in_library_items", false)
|
val hideInLibraryItems: Preference<Boolean> = preferenceStore.getBoolean("browse_hide_in_library_items", false)
|
||||||
|
|
||||||
fun extensionRepos() = preferenceStore.getStringSet("extension_repos", emptySet())
|
val extensionRepos: Preference<Set<String>> = preferenceStore.getStringSet("extension_repos", emptySet())
|
||||||
|
|
||||||
fun extensionUpdatesCount() = preferenceStore.getInt("ext_updates_count", 0)
|
val extensionUpdatesCount: Preference<Int> = preferenceStore.getInt("ext_updates_count", 0)
|
||||||
|
|
||||||
fun trustedExtensions() = preferenceStore.getStringSet(
|
val trustedExtensions: Preference<Set<String>> = preferenceStore.getStringSet(
|
||||||
Preference.appStateKey("trusted_extensions"),
|
Preference.appStateKey("trusted_extensions"),
|
||||||
emptySet(),
|
emptySet(),
|
||||||
)
|
)
|
||||||
|
|
||||||
fun globalSearchFilterState() = preferenceStore.getBoolean(
|
val globalSearchFilterState: Preference<Boolean> = preferenceStore.getBoolean(
|
||||||
Preference.appStateKey("has_filters_toggle_state"),
|
Preference.appStateKey("has_filters_toggle_state"),
|
||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun migrationSources() = preferenceStore.getLongArray("migration_sources", emptyList())
|
val migrationSources: Preference<List<Long>> = preferenceStore.getLongArray("migration_sources", emptyList())
|
||||||
|
|
||||||
fun migrationFlags() = preferenceStore.getObjectFromInt(
|
val migrationFlags: Preference<Set<MigrationFlag>> = preferenceStore.getObjectFromInt(
|
||||||
key = "migration_flags",
|
key = "migration_flags",
|
||||||
defaultValue = MigrationFlag.entries.toSet(),
|
defaultValue = MigrationFlag.entries.toSet(),
|
||||||
serializer = { MigrationFlag.toBit(it) },
|
serializer = { MigrationFlag.toBit(it) },
|
||||||
deserializer = { value: Int -> MigrationFlag.fromBit(value) },
|
deserializer = { value: Int -> MigrationFlag.fromBit(value) },
|
||||||
)
|
)
|
||||||
|
|
||||||
fun migrationDeepSearchMode() = preferenceStore.getBoolean("migration_deep_search", false)
|
val migrationDeepSearchMode: Preference<Boolean> = preferenceStore.getBoolean("migration_deep_search", false)
|
||||||
|
|
||||||
fun migrationPrioritizeByChapters() = preferenceStore.getBoolean("migration_prioritize_by_chapters", false)
|
val migrationPrioritizeByChapters: Preference<Boolean> = preferenceStore.getBoolean(
|
||||||
|
"migration_prioritize_by_chapters",
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
|
||||||
fun migrationHideUnmatched() = preferenceStore.getBoolean("migration_hide_unmatched", false)
|
val migrationHideUnmatched: Preference<Boolean> = preferenceStore.getBoolean("migration_hide_unmatched", false)
|
||||||
|
|
||||||
fun migrationHideWithoutUpdates() = preferenceStore.getBoolean("migration_hide_without_updates", false)
|
val migrationHideWithoutUpdates: Preference<Boolean> = preferenceStore.getBoolean(
|
||||||
|
"migration_hide_without_updates",
|
||||||
|
false,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package eu.kanade.domain.track.interactor
|
package eu.kanade.domain.track.interactor
|
||||||
|
|
||||||
|
import dev.zacsweers.metro.Inject
|
||||||
import eu.kanade.domain.track.model.toDbTrack
|
import eu.kanade.domain.track.model.toDbTrack
|
||||||
import eu.kanade.domain.track.model.toDomainTrack
|
import eu.kanade.domain.track.model.toDomainTrack
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
@@ -7,101 +8,92 @@ import eu.kanade.tachiyomi.data.track.EnhancedTracker
|
|||||||
import eu.kanade.tachiyomi.data.track.Tracker
|
import eu.kanade.tachiyomi.data.track.Tracker
|
||||||
import eu.kanade.tachiyomi.data.track.TrackerManager
|
import eu.kanade.tachiyomi.data.track.TrackerManager
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.util.lang.convertEpochMillisZone
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import tachiyomi.core.common.util.lang.withIOContext
|
|
||||||
import tachiyomi.core.common.util.lang.withNonCancellableContext
|
|
||||||
import tachiyomi.core.common.util.system.logcat
|
import tachiyomi.core.common.util.system.logcat
|
||||||
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
|
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
|
||||||
import tachiyomi.domain.history.interactor.GetHistory
|
import tachiyomi.domain.history.interactor.GetHistory
|
||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
import tachiyomi.domain.track.interactor.InsertTrack
|
import tachiyomi.domain.track.interactor.UpsertTrack
|
||||||
import uy.kohesive.injekt.Injekt
|
import kotlin.time.Instant
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
import java.time.ZoneOffset
|
|
||||||
|
|
||||||
|
@Inject
|
||||||
class AddTracks(
|
class AddTracks(
|
||||||
private val insertTrack: InsertTrack,
|
private val upsertTrack: UpsertTrack,
|
||||||
private val syncChapterProgressWithTrack: SyncChapterProgressWithTrack,
|
private val syncChapterProgressWithTrack: SyncChapterProgressWithTrack,
|
||||||
private val getChaptersByMangaId: GetChaptersByMangaId,
|
private val getChaptersByMangaId: GetChaptersByMangaId,
|
||||||
private val trackerManager: TrackerManager,
|
private val trackerManager: TrackerManager,
|
||||||
|
private val getHistory: GetHistory,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
// TODO: update all trackers based on common data
|
// TODO: update all trackers based on common data
|
||||||
suspend fun bind(tracker: Tracker, item: Track, mangaId: Long) = withNonCancellableContext {
|
suspend fun bind(tracker: Tracker, item: Track, mangaId: Long) = withContext(Dispatchers.IO) {
|
||||||
withIOContext {
|
val allChapters = getChaptersByMangaId.await(mangaId)
|
||||||
val allChapters = getChaptersByMangaId.await(mangaId)
|
val hasReadChapters = allChapters.any { it.read }
|
||||||
val hasReadChapters = allChapters.any { it.read }
|
tracker.bind(item, hasReadChapters)
|
||||||
tracker.bind(item, hasReadChapters)
|
|
||||||
|
|
||||||
var track = item.toDomainTrack(idRequired = false) ?: return@withIOContext
|
var track = item.toDomainTrack(idRequired = false) ?: return@withContext
|
||||||
|
|
||||||
insertTrack.await(track)
|
upsertTrack.await(track)
|
||||||
|
|
||||||
// TODO: merge into [SyncChapterProgressWithTrack]?
|
// TODO: merge into [SyncChapterProgressWithTrack]?
|
||||||
// Update chapter progress if newer chapters marked read locally
|
// Update chapter progress if newer chapters marked read locally
|
||||||
if (hasReadChapters) {
|
if (hasReadChapters) {
|
||||||
val latestLocalReadChapterNumber = allChapters
|
val latestLocalReadChapterNumber = allChapters
|
||||||
.sortedBy { it.chapterNumber }
|
.sortedBy { it.chapterNumber }
|
||||||
.takeWhile { it.read }
|
.takeWhile { it.read }
|
||||||
.lastOrNull()
|
.lastOrNull()
|
||||||
?.chapterNumber ?: -1.0
|
?.chapterNumber ?: -1.0
|
||||||
|
|
||||||
if (latestLocalReadChapterNumber > track.lastChapterRead) {
|
if (latestLocalReadChapterNumber > track.lastChapterRead) {
|
||||||
track = track.copy(
|
track = track.copy(
|
||||||
lastChapterRead = latestLocalReadChapterNumber,
|
lastChapterRead = latestLocalReadChapterNumber,
|
||||||
)
|
)
|
||||||
tracker.setRemoteLastChapterRead(track.toDbTrack(), latestLocalReadChapterNumber.toInt())
|
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)
|
if (track.startDate == null) {
|
||||||
|
val firstReadChapterDate = getHistory.await(mangaId)
|
||||||
|
.mapNotNull { it.readAt }
|
||||||
|
.minOrNull()
|
||||||
|
|
||||||
|
firstReadChapterDate?.let {
|
||||||
|
val startDate = firstReadChapterDate.toEpochMilliseconds()
|
||||||
|
track = track.copy(
|
||||||
|
startDate = Instant.fromEpochMilliseconds(startDate),
|
||||||
|
)
|
||||||
|
tracker.setRemoteStartDate(track.toDbTrack(), startDate)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
syncChapterProgressWithTrack.await(mangaId, track, tracker)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun bindEnhancedTrackers(manga: Manga, source: Source) = withNonCancellableContext {
|
suspend fun bindEnhancedTrackers(manga: Manga, source: Source) = withContext(Dispatchers.IO) {
|
||||||
withIOContext {
|
trackerManager.loggedInTrackers()
|
||||||
trackerManager.loggedInTrackers()
|
.filterIsInstance<EnhancedTracker>()
|
||||||
.filterIsInstance<EnhancedTracker>()
|
.filter { it.accept(source) }
|
||||||
.filter { it.accept(source) }
|
.forEach { service ->
|
||||||
.forEach { service ->
|
try {
|
||||||
try {
|
service.match(manga)?.let { track ->
|
||||||
service.match(manga)?.let { track ->
|
track.manga_id = manga.id
|
||||||
track.manga_id = manga.id
|
(service as Tracker).bind(track)
|
||||||
(service as Tracker).bind(track)
|
upsertTrack.await(track.toDomainTrack(idRequired = false)!!)
|
||||||
insertTrack.await(track.toDomainTrack(idRequired = false)!!)
|
|
||||||
|
|
||||||
syncChapterProgressWithTrack.await(
|
syncChapterProgressWithTrack.await(
|
||||||
manga.id,
|
manga.id,
|
||||||
track.toDomainTrack(idRequired = false)!!,
|
track.toDomainTrack(idRequired = false)!!,
|
||||||
service,
|
service,
|
||||||
)
|
)
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
logcat(
|
|
||||||
LogPriority.WARN,
|
|
||||||
e,
|
|
||||||
) { "Could not match manga: ${manga.title} with service $service" }
|
|
||||||
}
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logcat(
|
||||||
|
LogPriority.WARN,
|
||||||
|
e,
|
||||||
|
) { "Could not match manga: ${manga.title} with service $service" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,22 @@
|
|||||||
package eu.kanade.domain.track.interactor
|
package eu.kanade.domain.track.interactor
|
||||||
|
|
||||||
|
import dev.zacsweers.metro.Inject
|
||||||
import eu.kanade.domain.track.model.toDbTrack
|
import eu.kanade.domain.track.model.toDbTrack
|
||||||
import eu.kanade.domain.track.model.toDomainTrack
|
import eu.kanade.domain.track.model.toDomainTrack
|
||||||
import eu.kanade.tachiyomi.data.track.Tracker
|
import eu.kanade.tachiyomi.data.track.Tracker
|
||||||
import eu.kanade.tachiyomi.data.track.TrackerManager
|
import eu.kanade.tachiyomi.data.track.TrackerManager
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.awaitAll
|
import kotlinx.coroutines.awaitAll
|
||||||
import kotlinx.coroutines.supervisorScope
|
import kotlinx.coroutines.supervisorScope
|
||||||
import tachiyomi.domain.track.interactor.GetTracks
|
import tachiyomi.domain.track.interactor.GetTracks
|
||||||
import tachiyomi.domain.track.interactor.InsertTrack
|
import tachiyomi.domain.track.interactor.UpsertTrack
|
||||||
|
|
||||||
|
@Inject
|
||||||
class RefreshTracks(
|
class RefreshTracks(
|
||||||
private val getTracks: GetTracks,
|
private val getTracks: GetTracks,
|
||||||
private val trackerManager: TrackerManager,
|
private val trackerManager: TrackerManager,
|
||||||
private val insertTrack: InsertTrack,
|
private val upsertTrack: UpsertTrack,
|
||||||
private val syncChapterProgressWithTrack: SyncChapterProgressWithTrack,
|
private val syncChapterProgressWithTrack: SyncChapterProgressWithTrack,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@@ -28,10 +31,10 @@ class RefreshTracks(
|
|||||||
.map { it to trackerManager.get(it.trackerId) }
|
.map { it to trackerManager.get(it.trackerId) }
|
||||||
.filter { (_, service) -> service?.isLoggedIn == true }
|
.filter { (_, service) -> service?.isLoggedIn == true }
|
||||||
.map { (track, service) ->
|
.map { (track, service) ->
|
||||||
async {
|
async(Dispatchers.IO) {
|
||||||
return@async try {
|
return@async try {
|
||||||
val updatedTrack = service!!.refresh(track.toDbTrack()).toDomainTrack()!!
|
val updatedTrack = service!!.refresh(track.toDbTrack()).toDomainTrack()!!
|
||||||
insertTrack.await(updatedTrack)
|
upsertTrack.await(updatedTrack)
|
||||||
syncChapterProgressWithTrack.await(mangaId, updatedTrack, service)
|
syncChapterProgressWithTrack.await(mangaId, updatedTrack, service)
|
||||||
null
|
null
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package eu.kanade.domain.track.interactor
|
package eu.kanade.domain.track.interactor
|
||||||
|
|
||||||
|
import dev.zacsweers.metro.Inject
|
||||||
import eu.kanade.domain.track.model.toDbTrack
|
import eu.kanade.domain.track.model.toDbTrack
|
||||||
import eu.kanade.tachiyomi.data.track.EnhancedTracker
|
import eu.kanade.tachiyomi.data.track.EnhancedTracker
|
||||||
import eu.kanade.tachiyomi.data.track.Tracker
|
import eu.kanade.tachiyomi.data.track.Tracker
|
||||||
@@ -7,14 +8,15 @@ import logcat.LogPriority
|
|||||||
import tachiyomi.core.common.util.system.logcat
|
import tachiyomi.core.common.util.system.logcat
|
||||||
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
|
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
|
||||||
import tachiyomi.domain.chapter.interactor.UpdateChapter
|
import tachiyomi.domain.chapter.interactor.UpdateChapter
|
||||||
import tachiyomi.domain.chapter.model.toChapterUpdate
|
import tachiyomi.domain.chapter.model.ChapterUpdate
|
||||||
import tachiyomi.domain.track.interactor.InsertTrack
|
import tachiyomi.domain.track.interactor.UpsertTrack
|
||||||
import tachiyomi.domain.track.model.Track
|
import tachiyomi.domain.track.model.Track
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
|
|
||||||
|
@Inject
|
||||||
class SyncChapterProgressWithTrack(
|
class SyncChapterProgressWithTrack(
|
||||||
private val updateChapter: UpdateChapter,
|
private val updateChapter: UpdateChapter,
|
||||||
private val insertTrack: InsertTrack,
|
private val upsertTrack: UpsertTrack,
|
||||||
private val getChaptersByMangaId: GetChaptersByMangaId,
|
private val getChaptersByMangaId: GetChaptersByMangaId,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@@ -33,7 +35,7 @@ class SyncChapterProgressWithTrack(
|
|||||||
|
|
||||||
val chapterUpdates = sortedChapters
|
val chapterUpdates = sortedChapters
|
||||||
.filter { chapter -> chapter.chapterNumber <= remoteTrack.lastChapterRead && !chapter.read }
|
.filter { chapter -> chapter.chapterNumber <= remoteTrack.lastChapterRead && !chapter.read }
|
||||||
.map { it.copy(read = true).toChapterUpdate() }
|
.map { ChapterUpdate(it.id) { read = true } }
|
||||||
|
|
||||||
// only take into account continuous reading
|
// only take into account continuous reading
|
||||||
val localLastRead = sortedChapters.takeWhile { it.read }.lastOrNull()?.chapterNumber ?: 0F
|
val localLastRead = sortedChapters.takeWhile { it.read }.lastOrNull()?.chapterNumber ?: 0F
|
||||||
@@ -43,7 +45,7 @@ class SyncChapterProgressWithTrack(
|
|||||||
try {
|
try {
|
||||||
tracker.update(updatedTrack.toDbTrack())
|
tracker.update(updatedTrack.toDbTrack())
|
||||||
updateChapter.awaitAll(chapterUpdates)
|
updateChapter.awaitAll(chapterUpdates)
|
||||||
insertTrack.await(updatedTrack)
|
upsertTrack.await(updatedTrack)
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
logcat(LogPriority.WARN, e)
|
logcat(LogPriority.WARN, e)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,30 +1,32 @@
|
|||||||
package eu.kanade.domain.track.interactor
|
package eu.kanade.domain.track.interactor
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import dev.zacsweers.metro.Inject
|
||||||
import eu.kanade.domain.track.model.toDbTrack
|
import eu.kanade.domain.track.model.toDbTrack
|
||||||
import eu.kanade.domain.track.model.toDomainTrack
|
import eu.kanade.domain.track.model.toDomainTrack
|
||||||
import eu.kanade.domain.track.service.DelayedTrackingUpdateJob
|
import eu.kanade.domain.track.service.DelayedTrackingUpdateWorker
|
||||||
import eu.kanade.domain.track.store.DelayedTrackingStore
|
import eu.kanade.domain.track.store.DelayedTrackingStore
|
||||||
import eu.kanade.tachiyomi.data.track.TrackerManager
|
import eu.kanade.tachiyomi.data.track.TrackerManager
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.awaitAll
|
import kotlinx.coroutines.awaitAll
|
||||||
|
import kotlinx.coroutines.coroutineScope
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import tachiyomi.core.common.util.lang.withNonCancellableContext
|
|
||||||
import tachiyomi.core.common.util.system.logcat
|
import tachiyomi.core.common.util.system.logcat
|
||||||
import tachiyomi.domain.track.interactor.GetTracks
|
import tachiyomi.domain.track.interactor.GetTracks
|
||||||
import tachiyomi.domain.track.interactor.InsertTrack
|
import tachiyomi.domain.track.interactor.UpsertTrack
|
||||||
|
|
||||||
|
@Inject
|
||||||
class TrackChapter(
|
class TrackChapter(
|
||||||
private val getTracks: GetTracks,
|
private val getTracks: GetTracks,
|
||||||
private val trackerManager: TrackerManager,
|
private val trackerManager: TrackerManager,
|
||||||
private val insertTrack: InsertTrack,
|
private val upsertTrack: UpsertTrack,
|
||||||
private val delayedTrackingStore: DelayedTrackingStore,
|
private val delayedTrackingStore: DelayedTrackingStore,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
suspend fun await(context: Context, mangaId: Long, chapterNumber: Double, setupJobOnFailure: Boolean = true) {
|
suspend fun await(context: Context, mangaId: Long, chapterNumber: Double, setupJobOnFailure: Boolean = true) {
|
||||||
withNonCancellableContext {
|
coroutineScope {
|
||||||
val tracks = getTracks.await(mangaId)
|
val tracks = getTracks.await(mangaId)
|
||||||
if (tracks.isEmpty()) return@withNonCancellableContext
|
if (tracks.isEmpty()) return@coroutineScope
|
||||||
|
|
||||||
tracks.mapNotNull { track ->
|
tracks.mapNotNull { track ->
|
||||||
val service = trackerManager.get(track.trackerId)
|
val service = trackerManager.get(track.trackerId)
|
||||||
@@ -39,12 +41,12 @@ class TrackChapter(
|
|||||||
.toDomainTrack(idRequired = true)!!
|
.toDomainTrack(idRequired = true)!!
|
||||||
.copy(lastChapterRead = chapterNumber)
|
.copy(lastChapterRead = chapterNumber)
|
||||||
service.update(updatedTrack.toDbTrack(), true)
|
service.update(updatedTrack.toDbTrack(), true)
|
||||||
insertTrack.await(updatedTrack)
|
upsertTrack.await(updatedTrack)
|
||||||
delayedTrackingStore.remove(track.id)
|
delayedTrackingStore.remove(track.id)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
delayedTrackingStore.add(track.id, chapterNumber)
|
delayedTrackingStore.add(track.id, chapterNumber)
|
||||||
if (setupJobOnFailure) {
|
if (setupJobOnFailure) {
|
||||||
DelayedTrackingUpdateJob.setupTask(context)
|
DelayedTrackingUpdateWorker.setupTask(context)
|
||||||
}
|
}
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package eu.kanade.domain.track.model
|
package eu.kanade.domain.track.model
|
||||||
|
|
||||||
|
import mihon.core.common.extensions.toEpochMillisOrZero
|
||||||
|
import mihon.core.common.extensions.toInstantOrNull
|
||||||
import tachiyomi.domain.track.model.Track
|
import tachiyomi.domain.track.model.Track
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track as DbTrack
|
import eu.kanade.tachiyomi.data.database.models.Track as DbTrack
|
||||||
|
|
||||||
@@ -25,8 +27,8 @@ fun Track.toDbTrack(): DbTrack = DbTrack.create(trackerId).also {
|
|||||||
it.status = status
|
it.status = status
|
||||||
it.score = score
|
it.score = score
|
||||||
it.tracking_url = remoteUrl
|
it.tracking_url = remoteUrl
|
||||||
it.started_reading_date = startDate
|
it.started_reading_date = startDate.toEpochMillisOrZero()
|
||||||
it.finished_reading_date = finishDate
|
it.finished_reading_date = finishDate.toEpochMillisOrZero()
|
||||||
it.private = private
|
it.private = private
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,8 +46,8 @@ fun DbTrack.toDomainTrack(idRequired: Boolean = true): Track? {
|
|||||||
status = status,
|
status = status,
|
||||||
score = score,
|
score = score,
|
||||||
remoteUrl = tracking_url,
|
remoteUrl = tracking_url,
|
||||||
startDate = started_reading_date,
|
startDate = started_reading_date.toInstantOrNull(),
|
||||||
finishDate = finished_reading_date,
|
finishDate = finished_reading_date.toInstantOrNull(),
|
||||||
private = private,
|
private = private,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-11
@@ -8,31 +8,38 @@ import androidx.work.ExistingWorkPolicy
|
|||||||
import androidx.work.NetworkType
|
import androidx.work.NetworkType
|
||||||
import androidx.work.OneTimeWorkRequestBuilder
|
import androidx.work.OneTimeWorkRequestBuilder
|
||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
|
import dev.zacsweers.metro.Inject
|
||||||
import eu.kanade.domain.track.interactor.TrackChapter
|
import eu.kanade.domain.track.interactor.TrackChapter
|
||||||
import eu.kanade.domain.track.store.DelayedTrackingStore
|
import eu.kanade.domain.track.store.DelayedTrackingStore
|
||||||
import eu.kanade.tachiyomi.util.system.workManager
|
import eu.kanade.tachiyomi.util.system.workManager
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import tachiyomi.core.common.util.lang.withIOContext
|
import mihon.app.di.AppGraph
|
||||||
|
import mihon.core.metro.metroGraph
|
||||||
import tachiyomi.core.common.util.system.logcat
|
import tachiyomi.core.common.util.system.logcat
|
||||||
import tachiyomi.domain.track.interactor.GetTracks
|
import tachiyomi.domain.track.interactor.GetTracks
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
class DelayedTrackingUpdateJob(private val context: Context, workerParams: WorkerParameters) :
|
class DelayedTrackingUpdateWorker(private val context: Context, workerParams: WorkerParameters) :
|
||||||
CoroutineWorker(context, workerParams) {
|
CoroutineWorker(context, workerParams) {
|
||||||
|
|
||||||
|
private val graph: AppGraph = context.metroGraph()
|
||||||
|
|
||||||
|
@Inject lateinit var getTracks: GetTracks
|
||||||
|
|
||||||
|
@Inject lateinit var trackChapter: TrackChapter
|
||||||
|
|
||||||
|
@Inject lateinit var delayedTrackingStore: DelayedTrackingStore
|
||||||
|
|
||||||
override suspend fun doWork(): Result {
|
override suspend fun doWork(): Result {
|
||||||
|
graph.inject(this)
|
||||||
|
|
||||||
if (runAttemptCount > 3) {
|
if (runAttemptCount > 3) {
|
||||||
return Result.failure()
|
return Result.failure()
|
||||||
}
|
}
|
||||||
|
|
||||||
val getTracks = Injekt.get<GetTracks>()
|
withContext(Dispatchers.IO) {
|
||||||
val trackChapter = Injekt.get<TrackChapter>()
|
|
||||||
|
|
||||||
val delayedTrackingStore = Injekt.get<DelayedTrackingStore>()
|
|
||||||
|
|
||||||
withIOContext {
|
|
||||||
delayedTrackingStore.getItems()
|
delayedTrackingStore.getItems()
|
||||||
.mapNotNull {
|
.mapNotNull {
|
||||||
val track = getTracks.awaitOne(it.trackId)
|
val track = getTracks.awaitOne(it.trackId)
|
||||||
@@ -60,7 +67,7 @@ class DelayedTrackingUpdateJob(private val context: Context, workerParams: Worke
|
|||||||
requiredNetworkType = NetworkType.CONNECTED,
|
requiredNetworkType = NetworkType.CONNECTED,
|
||||||
)
|
)
|
||||||
|
|
||||||
val request = OneTimeWorkRequestBuilder<DelayedTrackingUpdateJob>()
|
val request = OneTimeWorkRequestBuilder<DelayedTrackingUpdateWorker>()
|
||||||
.setConstraints(constraints)
|
.setConstraints(constraints)
|
||||||
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 5, TimeUnit.MINUTES)
|
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 5, TimeUnit.MINUTES)
|
||||||
.addTag(TAG)
|
.addTag(TAG)
|
||||||
@@ -1,12 +1,19 @@
|
|||||||
package eu.kanade.domain.track.service
|
package eu.kanade.domain.track.service
|
||||||
|
|
||||||
|
import dev.zacsweers.metro.AppScope
|
||||||
|
import dev.zacsweers.metro.Inject
|
||||||
|
import dev.zacsweers.metro.SingleIn
|
||||||
import eu.kanade.domain.track.model.AutoTrackState
|
import eu.kanade.domain.track.model.AutoTrackState
|
||||||
import eu.kanade.tachiyomi.data.track.Tracker
|
import eu.kanade.tachiyomi.data.track.Tracker
|
||||||
import eu.kanade.tachiyomi.data.track.anilist.Anilist
|
import eu.kanade.tachiyomi.data.track.anilist.Anilist
|
||||||
|
import eu.kanade.tachiyomi.data.track.kitsu.Kitsu
|
||||||
|
import eu.kanade.tachiyomi.data.track.mangabaka.MangaBaka
|
||||||
import tachiyomi.core.common.preference.Preference
|
import tachiyomi.core.common.preference.Preference
|
||||||
import tachiyomi.core.common.preference.PreferenceStore
|
import tachiyomi.core.common.preference.PreferenceStore
|
||||||
import tachiyomi.core.common.preference.getEnum
|
import tachiyomi.core.common.preference.getEnum
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
@SingleIn(AppScope::class)
|
||||||
class TrackPreferences(
|
class TrackPreferences(
|
||||||
private val preferenceStore: PreferenceStore,
|
private val preferenceStore: PreferenceStore,
|
||||||
) {
|
) {
|
||||||
@@ -16,6 +23,11 @@ class TrackPreferences(
|
|||||||
"",
|
"",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
fun trackDisplayUsername(tracker: Tracker) = preferenceStore.getString(
|
||||||
|
Preference.privateKey("pref_mangasync_displayname_${tracker.id}"),
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
|
||||||
fun trackPassword(tracker: Tracker) = preferenceStore.getString(
|
fun trackPassword(tracker: Tracker) = preferenceStore.getString(
|
||||||
Preference.privateKey("pref_mangasync_password_${tracker.id}"),
|
Preference.privateKey("pref_mangasync_password_${tracker.id}"),
|
||||||
"",
|
"",
|
||||||
@@ -34,11 +46,15 @@ class TrackPreferences(
|
|||||||
|
|
||||||
fun trackToken(tracker: Tracker) = preferenceStore.getString(Preference.privateKey("track_token_${tracker.id}"), "")
|
fun trackToken(tracker: Tracker) = preferenceStore.getString(Preference.privateKey("track_token_${tracker.id}"), "")
|
||||||
|
|
||||||
fun anilistScoreType() = preferenceStore.getString("anilist_score_type", Anilist.POINT_10)
|
val anilistScoreType: Preference<String> = preferenceStore.getString("anilist_score_type", Anilist.POINT_10)
|
||||||
|
|
||||||
fun autoUpdateTrack() = preferenceStore.getBoolean("pref_auto_update_manga_sync_key", true)
|
val kitsuScoreType: Preference<String> = preferenceStore.getString("kitsu_score_type", Kitsu.RATING_ADVANCED)
|
||||||
|
|
||||||
fun autoUpdateTrackOnMarkRead() = preferenceStore.getEnum(
|
val mangabakaScoreType: Preference<String> = preferenceStore.getString("mangabaka_score_type", MangaBaka.STEP_1)
|
||||||
|
|
||||||
|
val autoUpdateTrack: Preference<Boolean> = preferenceStore.getBoolean("pref_auto_update_manga_sync_key", true)
|
||||||
|
|
||||||
|
val autoUpdateTrackOnMarkRead: Preference<AutoTrackState> = preferenceStore.getEnum(
|
||||||
"pref_auto_update_manga_on_mark_read",
|
"pref_auto_update_manga_on_mark_read",
|
||||||
AutoTrackState.ALWAYS,
|
AutoTrackState.ALWAYS,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,15 +2,20 @@ package eu.kanade.domain.track.store
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
|
import dev.zacsweers.metro.AppScope
|
||||||
|
import dev.zacsweers.metro.Inject
|
||||||
|
import dev.zacsweers.metro.SingleIn
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import tachiyomi.core.common.util.system.logcat
|
import tachiyomi.core.common.util.system.logcat
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
@SingleIn(AppScope::class)
|
||||||
class DelayedTrackingStore(context: Context) {
|
class DelayedTrackingStore(context: Context) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Preference file where queued tracking updates are stored.
|
* Preference file where queued tracking updates are stored.
|
||||||
*/
|
*/
|
||||||
private val preferences = context.getSharedPreferences("tracking_queue", Context.MODE_PRIVATE)
|
private val preferences by lazy { context.getSharedPreferences("tracking_queue", Context.MODE_PRIVATE) }
|
||||||
|
|
||||||
fun add(trackId: Long, lastChapterRead: Double) {
|
fun add(trackId: Long, lastChapterRead: Double) {
|
||||||
val previousLastChapterRead = preferences.getFloat(trackId.toString(), 0f)
|
val previousLastChapterRead = preferences.getFloat(trackId.toString(), 0f)
|
||||||
|
|||||||
@@ -1,23 +1,29 @@
|
|||||||
package eu.kanade.domain.ui
|
package eu.kanade.domain.ui
|
||||||
|
|
||||||
|
import dev.zacsweers.metro.AppScope
|
||||||
|
import dev.zacsweers.metro.Inject
|
||||||
|
import dev.zacsweers.metro.SingleIn
|
||||||
import eu.kanade.domain.ui.model.AppTheme
|
import eu.kanade.domain.ui.model.AppTheme
|
||||||
import eu.kanade.domain.ui.model.TabletUiMode
|
import eu.kanade.domain.ui.model.TabletUiMode
|
||||||
import eu.kanade.domain.ui.model.ThemeMode
|
import eu.kanade.domain.ui.model.ThemeMode
|
||||||
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
||||||
import eu.kanade.tachiyomi.util.system.isDynamicColorAvailable
|
import eu.kanade.tachiyomi.util.system.isDynamicColorAvailable
|
||||||
|
import tachiyomi.core.common.preference.Preference
|
||||||
import tachiyomi.core.common.preference.PreferenceStore
|
import tachiyomi.core.common.preference.PreferenceStore
|
||||||
import tachiyomi.core.common.preference.getEnum
|
import tachiyomi.core.common.preference.getEnum
|
||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
import java.time.format.FormatStyle
|
import java.time.format.FormatStyle
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
@SingleIn(AppScope::class)
|
||||||
class UiPreferences(
|
class UiPreferences(
|
||||||
private val preferenceStore: PreferenceStore,
|
preferenceStore: PreferenceStore,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun themeMode() = preferenceStore.getEnum("pref_theme_mode_key", ThemeMode.SYSTEM)
|
val themeMode: Preference<ThemeMode> = preferenceStore.getEnum("pref_theme_mode_key", ThemeMode.SYSTEM)
|
||||||
|
|
||||||
fun appTheme() = preferenceStore.getEnum(
|
val appTheme: Preference<AppTheme> = preferenceStore.getEnum(
|
||||||
"pref_app_theme",
|
"pref_app_theme",
|
||||||
if (DeviceUtil.isDynamicColorAvailable) {
|
if (DeviceUtil.isDynamicColorAvailable) {
|
||||||
AppTheme.MONET
|
AppTheme.MONET
|
||||||
@@ -26,15 +32,15 @@ class UiPreferences(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
fun themeDarkAmoled() = preferenceStore.getBoolean("pref_theme_dark_amoled_key", false)
|
val themeDarkAmoled: Preference<Boolean> = preferenceStore.getBoolean("pref_theme_dark_amoled_key", false)
|
||||||
|
|
||||||
fun relativeTime() = preferenceStore.getBoolean("relative_time_v2", true)
|
val relativeTime: Preference<Boolean> = preferenceStore.getBoolean("relative_time_v2", true)
|
||||||
|
|
||||||
fun dateFormat() = preferenceStore.getString("app_date_format", "")
|
val dateFormat: Preference<String> = preferenceStore.getString("app_date_format", "")
|
||||||
|
|
||||||
fun tabletUiMode() = preferenceStore.getEnum("tablet_ui_mode", TabletUiMode.AUTOMATIC)
|
val tabletUiMode: Preference<TabletUiMode> = preferenceStore.getEnum("tablet_ui_mode", TabletUiMode.AUTOMATIC)
|
||||||
|
|
||||||
fun imagesInDescription() = preferenceStore.getBoolean("pref_render_images_description", true)
|
val imagesInDescription: Preference<Boolean> = preferenceStore.getBoolean("pref_render_images_description", true)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun dateFormat(format: String): DateTimeFormatter = when (format) {
|
fun dateFormat(format: String): DateTimeFormatter = when (format) {
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ enum class AppTheme(val titleRes: StringResource?) {
|
|||||||
TIDAL_WAVE(MR.strings.theme_tidalwave),
|
TIDAL_WAVE(MR.strings.theme_tidalwave),
|
||||||
YINYANG(MR.strings.theme_yinyang),
|
YINYANG(MR.strings.theme_yinyang),
|
||||||
YOTSUBA(MR.strings.theme_yotsuba),
|
YOTSUBA(MR.strings.theme_yotsuba),
|
||||||
|
TOKYONIGHT(MR.strings.theme_tokyonight),
|
||||||
MONOCHROME(MR.strings.theme_monochrome),
|
MONOCHROME(MR.strings.theme_monochrome),
|
||||||
|
|
||||||
// Deprecated
|
// Deprecated
|
||||||
|
|||||||
@@ -3,10 +3,6 @@ package eu.kanade.presentation.browse
|
|||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.grid.GridCells
|
import androidx.compose.foundation.lazy.grid.GridCells
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
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
|
import androidx.compose.material3.SnackbarDuration
|
||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.material3.SnackbarResult
|
import androidx.compose.material3.SnackbarResult
|
||||||
@@ -22,8 +18,11 @@ import eu.kanade.presentation.browse.components.BrowseSourceList
|
|||||||
import eu.kanade.presentation.components.AppBar
|
import eu.kanade.presentation.components.AppBar
|
||||||
import eu.kanade.presentation.util.formattedMessage
|
import eu.kanade.presentation.util.formattedMessage
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import mihon.icons.materialsymbols.MaterialSymbols
|
||||||
|
import mihon.icons.materialsymbols.automirroredrounded.Help
|
||||||
|
import mihon.icons.materialsymbols.rounded.Public
|
||||||
|
import mihon.icons.materialsymbols.rounded.Refresh
|
||||||
import tachiyomi.core.common.i18n.stringResource
|
import tachiyomi.core.common.i18n.stringResource
|
||||||
import tachiyomi.domain.library.model.LibraryDisplayMode
|
import tachiyomi.domain.library.model.LibraryDisplayMode
|
||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
@@ -86,28 +85,28 @@ fun BrowseSourceContent(
|
|||||||
else -> stringResource(MR.strings.no_results_found)
|
else -> stringResource(MR.strings.no_results_found)
|
||||||
},
|
},
|
||||||
actions = if (source is LocalSource) {
|
actions = if (source is LocalSource) {
|
||||||
persistentListOf(
|
listOf(
|
||||||
EmptyScreenAction(
|
EmptyScreenAction(
|
||||||
stringRes = MR.strings.local_source_help_guide,
|
stringRes = MR.strings.local_source_help_guide,
|
||||||
icon = Icons.AutoMirrored.Outlined.HelpOutline,
|
icon = MaterialSymbols.AutoMirroredRounded.Help,
|
||||||
onClick = onLocalSourceHelpClick,
|
onClick = onLocalSourceHelpClick,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
persistentListOf(
|
listOf(
|
||||||
EmptyScreenAction(
|
EmptyScreenAction(
|
||||||
stringRes = MR.strings.action_retry,
|
stringRes = MR.strings.action_retry,
|
||||||
icon = Icons.Outlined.Refresh,
|
icon = MaterialSymbols.Rounded.Refresh,
|
||||||
onClick = mangaList::refresh,
|
onClick = mangaList::refresh,
|
||||||
),
|
),
|
||||||
EmptyScreenAction(
|
EmptyScreenAction(
|
||||||
stringRes = MR.strings.action_open_in_web_view,
|
stringRes = MR.strings.action_open_in_web_view,
|
||||||
icon = Icons.Outlined.Public,
|
icon = MaterialSymbols.Rounded.Public,
|
||||||
onClick = onWebViewClick,
|
onClick = onWebViewClick,
|
||||||
),
|
),
|
||||||
EmptyScreenAction(
|
EmptyScreenAction(
|
||||||
stringRes = MR.strings.label_help,
|
stringRes = MR.strings.label_help,
|
||||||
icon = Icons.AutoMirrored.Outlined.HelpOutline,
|
icon = MaterialSymbols.AutoMirroredRounded.Help,
|
||||||
onClick = onHelpClick,
|
onClick = onHelpClick,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -14,9 +14,6 @@ import androidx.compose.foundation.layout.height
|
|||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.automirrored.outlined.Launch
|
|
||||||
import androidx.compose.material.icons.outlined.Settings
|
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
@@ -35,40 +32,41 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalUriHandler
|
import androidx.compose.ui.platform.LocalUriHandler
|
||||||
import androidx.compose.ui.res.vectorResource
|
|
||||||
import androidx.compose.ui.text.TextStyle
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import dev.icerock.moko.resources.StringResource
|
||||||
import eu.kanade.domain.extension.interactor.ExtensionSourceItem
|
import eu.kanade.domain.extension.interactor.ExtensionSourceItem
|
||||||
import eu.kanade.presentation.browse.components.ExtensionIcon
|
import eu.kanade.presentation.browse.components.ExtensionIcon
|
||||||
|
import eu.kanade.presentation.browse.components.ExtensionPill
|
||||||
|
import eu.kanade.presentation.browse.components.label
|
||||||
import eu.kanade.presentation.components.AppBar
|
import eu.kanade.presentation.components.AppBar
|
||||||
import eu.kanade.presentation.components.AppBarActions
|
import eu.kanade.presentation.components.AppBarActions
|
||||||
import eu.kanade.presentation.components.WarningBanner
|
import eu.kanade.presentation.components.WarningBanner
|
||||||
import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget
|
import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget
|
||||||
import eu.kanade.presentation.more.settings.widget.TrailingWidgetBuffer
|
import eu.kanade.presentation.more.settings.widget.TrailingWidgetBuffer
|
||||||
import eu.kanade.tachiyomi.R
|
|
||||||
import eu.kanade.tachiyomi.extension.model.Extension
|
import eu.kanade.tachiyomi.extension.model.Extension
|
||||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||||
import eu.kanade.tachiyomi.ui.browse.extension.details.ExtensionDetailsScreenModel
|
import eu.kanade.tachiyomi.ui.browse.extension.details.ExtensionDetailsViewModel
|
||||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||||
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import mihon.icons.materialsymbols.MaterialSymbols
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import mihon.icons.materialsymbols.rounded.Eyeglasses2
|
||||||
|
import mihon.icons.materialsymbols.rounded.Settings
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.components.ScrollbarLazyColumn
|
import tachiyomi.presentation.core.components.ScrollbarLazyColumn
|
||||||
import tachiyomi.presentation.core.components.material.Scaffold
|
import tachiyomi.presentation.core.components.material.Scaffold
|
||||||
import tachiyomi.presentation.core.components.material.padding
|
import tachiyomi.presentation.core.components.material.padding
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
import tachiyomi.presentation.core.screens.EmptyScreen
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ExtensionDetailsScreen(
|
fun ExtensionDetailsScreen(
|
||||||
navigateUp: () -> Unit,
|
navigateUp: () -> Unit,
|
||||||
state: ExtensionDetailsScreenModel.State,
|
state: ExtensionDetailsViewModel.State.Success,
|
||||||
onClickSourcePreferences: (sourceId: Long) -> Unit,
|
onClickSourcePreferences: (sourceId: Long) -> Unit,
|
||||||
onClickEnableAll: () -> Unit,
|
onClickEnableAll: () -> Unit,
|
||||||
onClickDisableAll: () -> Unit,
|
onClickDisableAll: () -> Unit,
|
||||||
@@ -78,15 +76,6 @@ fun ExtensionDetailsScreen(
|
|||||||
onClickIncognito: (Boolean) -> Unit,
|
onClickIncognito: (Boolean) -> Unit,
|
||||||
) {
|
) {
|
||||||
val uriHandler = LocalUriHandler.current
|
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(
|
Scaffold(
|
||||||
topBar = { scrollBehavior ->
|
topBar = { scrollBehavior ->
|
||||||
@@ -95,54 +84,34 @@ fun ExtensionDetailsScreen(
|
|||||||
navigateUp = navigateUp,
|
navigateUp = navigateUp,
|
||||||
actions = {
|
actions = {
|
||||||
AppBarActions(
|
AppBarActions(
|
||||||
actions = persistentListOf<AppBar.AppBarAction>().builder()
|
actions = listOf(
|
||||||
.apply {
|
AppBar.OverflowAction(
|
||||||
if (url != null) {
|
title = stringResource(MR.strings.action_enable_all),
|
||||||
add(
|
onClick = onClickEnableAll,
|
||||||
AppBar.Action(
|
),
|
||||||
title = stringResource(MR.strings.action_open_repo),
|
AppBar.OverflowAction(
|
||||||
icon = Icons.AutoMirrored.Outlined.Launch,
|
title = stringResource(MR.strings.action_disable_all),
|
||||||
onClick = {
|
onClick = onClickDisableAll,
|
||||||
uriHandler.openUri(url)
|
),
|
||||||
},
|
AppBar.OverflowAction(
|
||||||
),
|
title = stringResource(MR.strings.pref_clear_cookies),
|
||||||
)
|
onClick = onClickClearCookies,
|
||||||
}
|
),
|
||||||
addAll(
|
),
|
||||||
listOf(
|
|
||||||
AppBar.OverflowAction(
|
|
||||||
title = stringResource(MR.strings.action_enable_all),
|
|
||||||
onClick = onClickEnableAll,
|
|
||||||
),
|
|
||||||
AppBar.OverflowAction(
|
|
||||||
title = stringResource(MR.strings.action_disable_all),
|
|
||||||
onClick = onClickDisableAll,
|
|
||||||
),
|
|
||||||
AppBar.OverflowAction(
|
|
||||||
title = stringResource(MR.strings.pref_clear_cookies),
|
|
||||||
onClick = onClickClearCookies,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.build(),
|
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
scrollBehavior = scrollBehavior,
|
scrollBehavior = scrollBehavior,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
if (state.extension == null) {
|
|
||||||
EmptyScreen(
|
|
||||||
MR.strings.empty_screen,
|
|
||||||
modifier = Modifier.padding(paddingValues),
|
|
||||||
)
|
|
||||||
return@Scaffold
|
|
||||||
}
|
|
||||||
|
|
||||||
ExtensionDetails(
|
ExtensionDetails(
|
||||||
contentPadding = paddingValues,
|
contentPadding = paddingValues,
|
||||||
extension = state.extension,
|
extension = state.extension,
|
||||||
|
onClickStore = state.extension.store
|
||||||
|
?.contact
|
||||||
|
?.website
|
||||||
|
?.takeIf { it.isNotBlank() }
|
||||||
|
?.let { website -> { uriHandler.openUri(website) } },
|
||||||
sources = state.sources,
|
sources = state.sources,
|
||||||
incognitoMode = state.isIncognito,
|
incognitoMode = state.isIncognito,
|
||||||
onClickSourcePreferences = onClickSourcePreferences,
|
onClickSourcePreferences = onClickSourcePreferences,
|
||||||
@@ -156,8 +125,9 @@ fun ExtensionDetailsScreen(
|
|||||||
@Composable
|
@Composable
|
||||||
private fun ExtensionDetails(
|
private fun ExtensionDetails(
|
||||||
contentPadding: PaddingValues,
|
contentPadding: PaddingValues,
|
||||||
extension: Extension.Installed,
|
extension: Extension.Loaded,
|
||||||
sources: ImmutableList<ExtensionSourceItem>,
|
onClickStore: (() -> Unit)?,
|
||||||
|
sources: List<ExtensionSourceItem>,
|
||||||
incognitoMode: Boolean,
|
incognitoMode: Boolean,
|
||||||
onClickSourcePreferences: (sourceId: Long) -> Unit,
|
onClickSourcePreferences: (sourceId: Long) -> Unit,
|
||||||
onClickUninstall: () -> Unit,
|
onClickUninstall: () -> Unit,
|
||||||
@@ -165,7 +135,8 @@ private fun ExtensionDetails(
|
|||||||
onClickIncognito: (Boolean) -> Unit,
|
onClickIncognito: (Boolean) -> Unit,
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
var showNsfwWarning by remember { mutableStateOf(false) }
|
var showContentWarning by remember { mutableStateOf(false) }
|
||||||
|
val contentWarning = extension.contentWarning.label
|
||||||
|
|
||||||
ScrollbarLazyColumn(
|
ScrollbarLazyColumn(
|
||||||
contentPadding = contentPadding,
|
contentPadding = contentPadding,
|
||||||
@@ -188,8 +159,9 @@ private fun ExtensionDetails(
|
|||||||
}
|
}
|
||||||
Unit
|
Unit
|
||||||
}.takeIf { extension.isShared },
|
}.takeIf { extension.isShared },
|
||||||
onClickAgeRating = {
|
onClickStore = onClickStore,
|
||||||
showNsfwWarning = true
|
onClickContentWarning = {
|
||||||
|
showContentWarning = true
|
||||||
},
|
},
|
||||||
onExtIncognitoChange = onClickIncognito,
|
onExtIncognitoChange = onClickIncognito,
|
||||||
)
|
)
|
||||||
@@ -207,10 +179,12 @@ private fun ExtensionDetails(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (showNsfwWarning) {
|
if (showContentWarning && contentWarning != null) {
|
||||||
NsfwWarningDialog(
|
ContentWarningDialog(
|
||||||
|
label = contentWarning.title,
|
||||||
|
description = contentWarning.description,
|
||||||
onClickConfirm = {
|
onClickConfirm = {
|
||||||
showNsfwWarning = false
|
showContentWarning = false
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -218,14 +192,16 @@ private fun ExtensionDetails(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun DetailsHeader(
|
private fun DetailsHeader(
|
||||||
extension: Extension,
|
extension: Extension.Installed,
|
||||||
extIncognitoMode: Boolean,
|
extIncognitoMode: Boolean,
|
||||||
onClickAgeRating: () -> Unit,
|
onClickStore: (() -> Unit)?,
|
||||||
|
onClickContentWarning: () -> Unit,
|
||||||
onClickUninstall: () -> Unit,
|
onClickUninstall: () -> Unit,
|
||||||
onClickAppInfo: (() -> Unit)?,
|
onClickAppInfo: (() -> Unit)?,
|
||||||
onExtIncognitoChange: (Boolean) -> Unit,
|
onExtIncognitoChange: (Boolean) -> Unit,
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
val contentWarning = extension.contentWarning.label
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
Column(
|
Column(
|
||||||
@@ -242,20 +218,23 @@ private fun DetailsHeader(
|
|||||||
"""
|
"""
|
||||||
Extension name: ${extension.name} (lang: ${extension.lang}; package: ${extension.pkgName})
|
Extension name: ${extension.name} (lang: ${extension.lang}; package: ${extension.pkgName})
|
||||||
Extension version: ${extension.versionName} (lib: ${extension.libVersion}; version code: ${extension.versionCode})
|
Extension version: ${extension.versionName} (lib: ${extension.libVersion}; version code: ${extension.versionCode})
|
||||||
NSFW: ${extension.isNsfw}
|
Content warning: ${extension.contentWarning}
|
||||||
""".trimIndent(),
|
""".trimIndent(),
|
||||||
)
|
)
|
||||||
|
|
||||||
if (extension is Extension.Installed) {
|
if (extension is Extension.Loaded) {
|
||||||
append("\n\n")
|
append("\n\n")
|
||||||
append(
|
appendLine(
|
||||||
"""
|
"""
|
||||||
Update available: ${extension.hasUpdate}
|
Update available: ${extension.hasUpdate}
|
||||||
Obsolete: ${extension.isObsolete}
|
Orphaned: ${extension.isObsolete}
|
||||||
Shared: ${extension.isShared}
|
Shared: ${extension.isShared}
|
||||||
Repository: ${extension.repoUrl}
|
|
||||||
""".trimIndent(),
|
""".trimIndent(),
|
||||||
)
|
)
|
||||||
|
val store = extension.store
|
||||||
|
if (store != null) {
|
||||||
|
append("Repository: ${store.indexUrl}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
context.copyToClipboard("Extension Debug information", extDebugInfo)
|
context.copyToClipboard("Extension Debug information", extDebugInfo)
|
||||||
@@ -275,19 +254,42 @@ private fun DetailsHeader(
|
|||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
)
|
)
|
||||||
|
|
||||||
val strippedPkgName = extension.pkgName.substringAfter("eu.kanade.tachiyomi.extension.")
|
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = strippedPkgName,
|
text = extension.pkgName,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
extension.store?.let { store ->
|
||||||
|
Text(
|
||||||
|
text = store.name,
|
||||||
|
modifier = Modifier
|
||||||
|
.clip(MaterialTheme.shapes.small)
|
||||||
|
.clickable(enabled = onClickStore != null) { onClickStore?.invoke() }
|
||||||
|
.padding(
|
||||||
|
horizontal = MaterialTheme.padding.extraSmall,
|
||||||
|
vertical = MaterialTheme.padding.extraSmall / 2,
|
||||||
|
),
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
style = MaterialTheme.typography.labelLarge,
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((extension as? Extension.Loaded)?.isShared == false) {
|
||||||
|
ExtensionPill(
|
||||||
|
text = stringResource(MR.strings.ext_installer_private),
|
||||||
|
modifier = Modifier.padding(top = MaterialTheme.padding.small),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(
|
.padding(
|
||||||
horizontal = MaterialTheme.padding.extraLarge,
|
horizontal = MaterialTheme.padding.medium,
|
||||||
vertical = MaterialTheme.padding.small,
|
vertical = MaterialTheme.padding.small,
|
||||||
),
|
),
|
||||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||||
@@ -302,23 +304,20 @@ private fun DetailsHeader(
|
|||||||
InfoDivider()
|
InfoDivider()
|
||||||
|
|
||||||
InfoText(
|
InfoText(
|
||||||
modifier = Modifier.weight(if (extension.isNsfw) 1.5f else 1f),
|
modifier = Modifier.weight(1f),
|
||||||
primaryText = LocaleHelper.getSourceDisplayName(extension.lang, context),
|
primaryText = LocaleHelper.getSourceDisplayName(extension.lang, context),
|
||||||
secondaryText = stringResource(MR.strings.ext_info_language),
|
secondaryText = stringResource(MR.strings.ext_info_language),
|
||||||
)
|
)
|
||||||
|
|
||||||
if (extension.isNsfw) {
|
if (contentWarning != null) {
|
||||||
InfoDivider()
|
InfoDivider()
|
||||||
|
|
||||||
InfoText(
|
InfoText(
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
primaryText = stringResource(MR.strings.ext_nsfw_short),
|
primaryText = stringResource(contentWarning.title),
|
||||||
primaryTextStyle = MaterialTheme.typography.bodyLarge.copy(
|
primaryTextColor = contentWarning.color,
|
||||||
color = MaterialTheme.colorScheme.error,
|
secondaryText = stringResource(MR.strings.ext_info_warning),
|
||||||
fontWeight = FontWeight.Medium,
|
onClick = onClickContentWarning,
|
||||||
),
|
|
||||||
secondaryText = stringResource(MR.strings.ext_info_age_rating),
|
|
||||||
onClick = onClickAgeRating,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -341,10 +340,7 @@ private fun DetailsHeader(
|
|||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
onClick = onClickAppInfo,
|
onClick = onClickAppInfo,
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(stringResource(MR.strings.ext_app_info))
|
||||||
text = stringResource(MR.strings.ext_app_info),
|
|
||||||
color = MaterialTheme.colorScheme.onPrimary,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -353,7 +349,7 @@ private fun DetailsHeader(
|
|||||||
modifier = Modifier.padding(horizontal = MaterialTheme.padding.small),
|
modifier = Modifier.padding(horizontal = MaterialTheme.padding.small),
|
||||||
title = stringResource(MR.strings.pref_incognito_mode),
|
title = stringResource(MR.strings.pref_incognito_mode),
|
||||||
subtitle = stringResource(MR.strings.pref_incognito_mode_extension_summary),
|
subtitle = stringResource(MR.strings.pref_incognito_mode_extension_summary),
|
||||||
icon = ImageVector.vectorResource(R.drawable.ic_glasses_24dp),
|
icon = MaterialSymbols.Rounded.Eyeglasses2,
|
||||||
widget = {
|
widget = {
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
@@ -376,7 +372,7 @@ private fun InfoText(
|
|||||||
primaryText: String,
|
primaryText: String,
|
||||||
secondaryText: String,
|
secondaryText: String,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
primaryTextStyle: TextStyle = MaterialTheme.typography.bodyLarge,
|
primaryTextColor: Color = Color.Unspecified,
|
||||||
onClick: (() -> Unit)? = null,
|
onClick: (() -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
val clickableModifier = if (onClick != null) {
|
val clickableModifier = if (onClick != null) {
|
||||||
@@ -393,13 +389,16 @@ private fun InfoText(
|
|||||||
Text(
|
Text(
|
||||||
text = primaryText,
|
text = primaryText,
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
style = primaryTextStyle,
|
style = MaterialTheme.typography.titleSmall,
|
||||||
|
color = primaryTextColor,
|
||||||
|
maxLines = 2,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
)
|
)
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = secondaryText + if (onClick != null) " ⓘ" else "",
|
text = secondaryText + if (onClick != null) " ⓘ" else "",
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f),
|
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -408,7 +407,9 @@ private fun InfoText(
|
|||||||
@Composable
|
@Composable
|
||||||
private fun InfoDivider() {
|
private fun InfoDivider() {
|
||||||
VerticalDivider(
|
VerticalDivider(
|
||||||
modifier = Modifier.height(20.dp),
|
modifier = Modifier
|
||||||
|
.padding(horizontal = MaterialTheme.padding.small)
|
||||||
|
.height(24.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -435,7 +436,7 @@ private fun SourceSwitchPreference(
|
|||||||
if (source.source is ConfigurableSource) {
|
if (source.source is ConfigurableSource) {
|
||||||
IconButton(onClick = { onClickSourcePreferences(source.source.id) }) {
|
IconButton(onClick = { onClickSourcePreferences(source.source.id) }) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Outlined.Settings,
|
imageVector = MaterialSymbols.Rounded.Settings,
|
||||||
contentDescription = stringResource(MR.strings.label_settings),
|
contentDescription = stringResource(MR.strings.label_settings),
|
||||||
tint = MaterialTheme.colorScheme.onSurface,
|
tint = MaterialTheme.colorScheme.onSurface,
|
||||||
)
|
)
|
||||||
@@ -454,12 +455,20 @@ private fun SourceSwitchPreference(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun NsfwWarningDialog(
|
private fun ContentWarningDialog(
|
||||||
|
label: StringResource,
|
||||||
|
description: StringResource,
|
||||||
onClickConfirm: () -> Unit,
|
onClickConfirm: () -> Unit,
|
||||||
) {
|
) {
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(label),
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
)
|
||||||
|
},
|
||||||
text = {
|
text = {
|
||||||
Text(text = stringResource(MR.strings.ext_nsfw_warning))
|
Text(text = stringResource(description))
|
||||||
},
|
},
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
TextButton(onClick = onClickConfirm) {
|
TextButton(onClick = onClickConfirm) {
|
||||||
|
|||||||
@@ -13,13 +13,7 @@ import androidx.compose.foundation.layout.RowScope
|
|||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
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.AlertDialog
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
@@ -27,7 +21,6 @@ import androidx.compose.material3.Icon
|
|||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.LocalTextStyle
|
import androidx.compose.material3.LocalTextStyle
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.ProvideTextStyle
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -37,7 +30,9 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||||
@@ -45,18 +40,28 @@ import cafe.adriel.voyager.navigator.currentOrThrow
|
|||||||
import dev.icerock.moko.resources.StringResource
|
import dev.icerock.moko.resources.StringResource
|
||||||
import eu.kanade.presentation.browse.components.BaseBrowseItem
|
import eu.kanade.presentation.browse.components.BaseBrowseItem
|
||||||
import eu.kanade.presentation.browse.components.ExtensionIcon
|
import eu.kanade.presentation.browse.components.ExtensionIcon
|
||||||
|
import eu.kanade.presentation.browse.components.ExtensionPill
|
||||||
|
import eu.kanade.presentation.browse.components.label
|
||||||
import eu.kanade.presentation.components.WarningBanner
|
import eu.kanade.presentation.components.WarningBanner
|
||||||
import eu.kanade.presentation.manga.components.DotSeparatorNoSpaceText
|
import eu.kanade.presentation.more.settings.screen.browse.ExtensionStoresScreen
|
||||||
import eu.kanade.presentation.more.settings.screen.browse.ExtensionReposScreen
|
|
||||||
import eu.kanade.presentation.util.animateItemFastScroll
|
|
||||||
import eu.kanade.presentation.util.rememberRequestPackageInstallsPermissionState
|
import eu.kanade.presentation.util.rememberRequestPackageInstallsPermissionState
|
||||||
import eu.kanade.tachiyomi.extension.model.Extension
|
import eu.kanade.tachiyomi.extension.model.Extension
|
||||||
import eu.kanade.tachiyomi.extension.model.InstallStep
|
import eu.kanade.tachiyomi.extension.model.InstallStep
|
||||||
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionUiModel
|
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionUiModel
|
||||||
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionsScreenModel
|
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionsViewModel
|
||||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||||
|
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
||||||
import eu.kanade.tachiyomi.util.system.launchRequestPackageInstallsPermission
|
import eu.kanade.tachiyomi.util.system.launchRequestPackageInstallsPermission
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import mihon.domain.extension.model.ExtensionStore
|
||||||
|
import mihon.icons.materialsymbols.MaterialSymbols
|
||||||
|
import mihon.icons.materialsymbols.rounded.Close
|
||||||
|
import mihon.icons.materialsymbols.rounded.Download
|
||||||
|
import mihon.icons.materialsymbols.rounded.Info
|
||||||
|
import mihon.icons.materialsymbols.rounded.Public
|
||||||
|
import mihon.icons.materialsymbols.rounded.Refresh
|
||||||
|
import mihon.icons.materialsymbols.rounded.Settings
|
||||||
|
import mihon.icons.materialsymbols.rounded.VerifiedUser
|
||||||
|
import tachiyomi.core.common.i18n.stringResource
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.components.FastScrollLazyColumn
|
import tachiyomi.presentation.core.components.FastScrollLazyColumn
|
||||||
import tachiyomi.presentation.core.components.material.PullRefresh
|
import tachiyomi.presentation.core.components.material.PullRefresh
|
||||||
@@ -68,21 +73,20 @@ import tachiyomi.presentation.core.screens.EmptyScreenAction
|
|||||||
import tachiyomi.presentation.core.screens.LoadingScreen
|
import tachiyomi.presentation.core.screens.LoadingScreen
|
||||||
import tachiyomi.presentation.core.theme.header
|
import tachiyomi.presentation.core.theme.header
|
||||||
import tachiyomi.presentation.core.util.plus
|
import tachiyomi.presentation.core.util.plus
|
||||||
import tachiyomi.presentation.core.util.secondaryItemAlpha
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ExtensionScreen(
|
fun ExtensionScreen(
|
||||||
state: ExtensionsScreenModel.State,
|
state: ExtensionsViewModel.State,
|
||||||
contentPadding: PaddingValues,
|
contentPadding: PaddingValues,
|
||||||
searchQuery: String?,
|
searchQuery: String?,
|
||||||
onLongClickItem: (Extension) -> Unit,
|
onLongClickItem: (Extension) -> Unit,
|
||||||
onClickItemCancel: (Extension) -> Unit,
|
onClickItemCancel: (Extension) -> Unit,
|
||||||
onOpenWebView: (Extension.Available) -> Unit,
|
onOpenWebView: (Extension.Available) -> Unit,
|
||||||
onInstallExtension: (Extension.Available) -> Unit,
|
onInstallExtension: (Extension.Available) -> Unit,
|
||||||
onUninstallExtension: (Extension) -> Unit,
|
onUninstallExtension: (Extension.Installed) -> Unit,
|
||||||
onUpdateExtension: (Extension.Installed) -> Unit,
|
onUpdateExtension: (Extension.Installed) -> Unit,
|
||||||
onTrustExtension: (Extension.Untrusted) -> Unit,
|
onTrustExtension: (Extension.NotLoaded) -> Unit,
|
||||||
onOpenExtension: (Extension.Installed) -> Unit,
|
onOpenExtension: (Extension.Loaded) -> Unit,
|
||||||
onClickUpdateAll: () -> Unit,
|
onClickUpdateAll: () -> Unit,
|
||||||
onRefresh: () -> Unit,
|
onRefresh: () -> Unit,
|
||||||
) {
|
) {
|
||||||
@@ -104,11 +108,11 @@ fun ExtensionScreen(
|
|||||||
EmptyScreen(
|
EmptyScreen(
|
||||||
stringRes = msg,
|
stringRes = msg,
|
||||||
modifier = Modifier.padding(contentPadding),
|
modifier = Modifier.padding(contentPadding),
|
||||||
actions = persistentListOf(
|
actions = listOf(
|
||||||
EmptyScreenAction(
|
EmptyScreenAction(
|
||||||
stringRes = MR.strings.label_extension_repos,
|
stringRes = MR.strings.extensionStores,
|
||||||
icon = Icons.Outlined.Settings,
|
icon = MaterialSymbols.Rounded.Settings,
|
||||||
onClick = { navigator.push(ExtensionReposScreen()) },
|
onClick = { navigator.push(ExtensionStoresScreen()) },
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -134,20 +138,20 @@ fun ExtensionScreen(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ExtensionContent(
|
private fun ExtensionContent(
|
||||||
state: ExtensionsScreenModel.State,
|
state: ExtensionsViewModel.State,
|
||||||
contentPadding: PaddingValues,
|
contentPadding: PaddingValues,
|
||||||
onLongClickItem: (Extension) -> Unit,
|
onLongClickItem: (Extension) -> Unit,
|
||||||
onClickItemCancel: (Extension) -> Unit,
|
onClickItemCancel: (Extension) -> Unit,
|
||||||
onOpenWebView: (Extension.Available) -> Unit,
|
onOpenWebView: (Extension.Available) -> Unit,
|
||||||
onInstallExtension: (Extension.Available) -> Unit,
|
onInstallExtension: (Extension.Available) -> Unit,
|
||||||
onUninstallExtension: (Extension) -> Unit,
|
onUninstallExtension: (Extension.Installed) -> Unit,
|
||||||
onUpdateExtension: (Extension.Installed) -> Unit,
|
onUpdateExtension: (Extension.Installed) -> Unit,
|
||||||
onTrustExtension: (Extension.Untrusted) -> Unit,
|
onTrustExtension: (Extension.NotLoaded) -> Unit,
|
||||||
onOpenExtension: (Extension.Installed) -> Unit,
|
onOpenExtension: (Extension.Loaded) -> Unit,
|
||||||
onClickUpdateAll: () -> Unit,
|
onClickUpdateAll: () -> Unit,
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
var trustState by remember { mutableStateOf<Extension.Untrusted?>(null) }
|
var notLoadedState by remember { mutableStateOf<Extension.NotLoaded?>(null) }
|
||||||
val installGranted = rememberRequestPackageInstallsPermissionState(initialValue = true)
|
val installGranted = rememberRequestPackageInstallsPermissionState(initialValue = true)
|
||||||
|
|
||||||
FastScrollLazyColumn(
|
FastScrollLazyColumn(
|
||||||
@@ -188,14 +192,14 @@ private fun ExtensionContent(
|
|||||||
}
|
}
|
||||||
ExtensionHeader(
|
ExtensionHeader(
|
||||||
textRes = header.textRes,
|
textRes = header.textRes,
|
||||||
modifier = Modifier.animateItemFastScroll(),
|
modifier = Modifier.animateItem(),
|
||||||
action = action,
|
action = action,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is ExtensionUiModel.Header.Text -> {
|
is ExtensionUiModel.Header.Text -> {
|
||||||
ExtensionHeader(
|
ExtensionHeader(
|
||||||
text = header.text,
|
text = header.text,
|
||||||
modifier = Modifier.animateItemFastScroll(),
|
modifier = Modifier.animateItem(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -206,21 +210,21 @@ private fun ExtensionContent(
|
|||||||
contentType = { "item" },
|
contentType = { "item" },
|
||||||
key = { item ->
|
key = { item ->
|
||||||
when (item.extension) {
|
when (item.extension) {
|
||||||
is Extension.Untrusted -> "extension-untrusted-${item.hashCode()}"
|
is Extension.NotLoaded -> "extension-not-loaded-${item.hashCode()}"
|
||||||
is Extension.Installed -> "extension-installed-${item.hashCode()}"
|
is Extension.Loaded -> "extension-loaded-${item.hashCode()}"
|
||||||
is Extension.Available -> "extension-available-${item.hashCode()}"
|
is Extension.Available -> "extension-available-${item.hashCode()}"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
) { item ->
|
) { item ->
|
||||||
ExtensionItem(
|
ExtensionItem(
|
||||||
modifier = Modifier.animateItemFastScroll(),
|
modifier = Modifier.animateItem(),
|
||||||
item = item,
|
item = item,
|
||||||
onClickItem = {
|
onClickItem = {
|
||||||
when (it) {
|
when (it) {
|
||||||
is Extension.Available -> onInstallExtension(it)
|
is Extension.Available -> onInstallExtension(it)
|
||||||
is Extension.Installed -> onOpenExtension(it)
|
is Extension.Loaded -> onOpenExtension(it)
|
||||||
is Extension.Untrusted -> {
|
is Extension.NotLoaded -> {
|
||||||
trustState = it
|
notLoadedState = it
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -228,23 +232,29 @@ private fun ExtensionContent(
|
|||||||
onClickItemSecondaryAction = {
|
onClickItemSecondaryAction = {
|
||||||
when (it) {
|
when (it) {
|
||||||
is Extension.Available -> onOpenWebView(it)
|
is Extension.Available -> onOpenWebView(it)
|
||||||
is Extension.Installed -> onOpenExtension(it)
|
is Extension.Loaded -> onOpenExtension(it)
|
||||||
else -> {}
|
is Extension.NotLoaded -> {
|
||||||
|
notLoadedState = it
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onClickItemCancel = onClickItemCancel,
|
onClickItemCancel = onClickItemCancel,
|
||||||
onClickItemAction = {
|
onClickItemAction = {
|
||||||
when (it) {
|
when (it) {
|
||||||
is Extension.Available -> onInstallExtension(it)
|
is Extension.Available -> onInstallExtension(it)
|
||||||
is Extension.Installed -> {
|
is Extension.Loaded -> {
|
||||||
if (it.hasUpdate) {
|
if (it.hasUpdate) {
|
||||||
onUpdateExtension(it)
|
onUpdateExtension(it)
|
||||||
} else {
|
} else {
|
||||||
onOpenExtension(it)
|
onOpenExtension(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is Extension.Untrusted -> {
|
is Extension.NotLoaded -> {
|
||||||
trustState = it
|
if (it.hasUpdate) {
|
||||||
|
onUpdateExtension(it)
|
||||||
|
} else {
|
||||||
|
notLoadedState = it
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -252,20 +262,30 @@ private fun ExtensionContent(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (trustState != null) {
|
notLoadedState?.let { extension ->
|
||||||
ExtensionTrustDialog(
|
val dismiss = { notLoadedState = null }
|
||||||
onClickConfirm = {
|
if (extension.reason is Extension.NotLoaded.Reason.Untrusted) {
|
||||||
onTrustExtension(trustState!!)
|
ExtensionTrustDialog(
|
||||||
trustState = null
|
onClickConfirm = {
|
||||||
},
|
onTrustExtension(extension)
|
||||||
onClickDismiss = {
|
dismiss()
|
||||||
onUninstallExtension(trustState!!)
|
},
|
||||||
trustState = null
|
onClickDismiss = {
|
||||||
},
|
onUninstallExtension(extension)
|
||||||
onDismissRequest = {
|
dismiss()
|
||||||
trustState = null
|
},
|
||||||
},
|
onDismissRequest = dismiss,
|
||||||
)
|
)
|
||||||
|
} else {
|
||||||
|
ExtensionNotLoadedDialog(
|
||||||
|
reason = extension.reason,
|
||||||
|
onClickUninstall = {
|
||||||
|
onUninstallExtension(extension)
|
||||||
|
dismiss()
|
||||||
|
},
|
||||||
|
onDismissRequest = dismiss,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -280,39 +300,17 @@ private fun ExtensionItem(
|
|||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val (extension, installStep) = item
|
val (extension, installStep) = item
|
||||||
|
val store = when (extension) {
|
||||||
|
is Extension.Available -> extension.store
|
||||||
|
is Extension.Installed -> extension.store
|
||||||
|
}
|
||||||
|
|
||||||
BaseBrowseItem(
|
BaseBrowseItem(
|
||||||
modifier = modifier
|
modifier = modifier,
|
||||||
.combinedClickable(
|
|
||||||
onClick = { onClickItem(extension) },
|
|
||||||
onLongClick = { onLongClickItem(extension) },
|
|
||||||
),
|
|
||||||
onClickItem = { onClickItem(extension) },
|
onClickItem = { onClickItem(extension) },
|
||||||
onLongClickItem = { onLongClickItem(extension) },
|
onLongClickItem = { onLongClickItem(extension) },
|
||||||
icon = {
|
icon = {
|
||||||
Box(
|
ExtensionItemIcon(extension = extension, installStep = installStep)
|
||||||
modifier = Modifier
|
|
||||||
.size(40.dp),
|
|
||||||
contentAlignment = Alignment.Center,
|
|
||||||
) {
|
|
||||||
val idle = installStep.isCompleted()
|
|
||||||
if (!idle) {
|
|
||||||
CircularProgressIndicator(
|
|
||||||
modifier = Modifier.size(40.dp),
|
|
||||||
strokeWidth = 2.dp,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val padding by animateDpAsState(
|
|
||||||
targetValue = if (idle) 0.dp else 8.dp,
|
|
||||||
label = "iconPadding",
|
|
||||||
)
|
|
||||||
ExtensionIcon(
|
|
||||||
extension = extension,
|
|
||||||
modifier = Modifier
|
|
||||||
.matchParentSize()
|
|
||||||
.padding(padding),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
action = {
|
action = {
|
||||||
ExtensionItemActions(
|
ExtensionItemActions(
|
||||||
@@ -327,6 +325,7 @@ private fun ExtensionItem(
|
|||||||
ExtensionItemContent(
|
ExtensionItemContent(
|
||||||
extension = extension,
|
extension = extension,
|
||||||
installStep = installStep,
|
installStep = installStep,
|
||||||
|
store = store,
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -336,6 +335,7 @@ private fun ExtensionItem(
|
|||||||
private fun ExtensionItemContent(
|
private fun ExtensionItemContent(
|
||||||
extension: Extension,
|
extension: Extension,
|
||||||
installStep: InstallStep,
|
installStep: InstallStep,
|
||||||
|
store: ExtensionStore?,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
@@ -347,63 +347,106 @@ private fun ExtensionItemContent(
|
|||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
)
|
)
|
||||||
// Won't look good but it's not like we can ellipsize overflowing content
|
|
||||||
FlowRow(
|
|
||||||
modifier = Modifier.secondaryItemAlpha(),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
|
|
||||||
) {
|
|
||||||
ProvideTextStyle(value = MaterialTheme.typography.bodySmall) {
|
|
||||||
var hasAlreadyShownAnElement by remember { mutableStateOf(false) }
|
|
||||||
if (extension is Extension.Installed && extension.lang.isNotEmpty()) {
|
|
||||||
hasAlreadyShownAnElement = true
|
|
||||||
Text(
|
|
||||||
text = LocaleHelper.getSourceDisplayName(extension.lang, LocalContext.current),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (extension.versionName.isNotEmpty()) {
|
if (store != null) {
|
||||||
if (hasAlreadyShownAnElement) DotSeparatorNoSpaceText()
|
Text(
|
||||||
hasAlreadyShownAnElement = true
|
text = store.name,
|
||||||
Text(
|
maxLines = 1,
|
||||||
text = extension.versionName,
|
overflow = TextOverflow.Ellipsis,
|
||||||
)
|
style = MaterialTheme.typography.bodySmall,
|
||||||
}
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
val warning = when {
|
ExtensionItemMetadata(extension = extension, installStep = installStep)
|
||||||
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) {
|
|
||||||
if (hasAlreadyShownAnElement) DotSeparatorNoSpaceText()
|
|
||||||
hasAlreadyShownAnElement = true
|
|
||||||
Text(
|
|
||||||
text = stringResource(warning).uppercase(),
|
|
||||||
color = MaterialTheme.colorScheme.error,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (extension is Extension.Installed && !extension.isShared) {
|
|
||||||
if (hasAlreadyShownAnElement) DotSeparatorNoSpaceText()
|
|
||||||
Text(
|
|
||||||
text = stringResource(MR.strings.ext_installer_private),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!installStep.isCompleted()) {
|
@Composable
|
||||||
DotSeparatorNoSpaceText()
|
private fun ExtensionItemIcon(
|
||||||
Text(
|
extension: Extension,
|
||||||
text = when (installStep) {
|
installStep: InstallStep,
|
||||||
InstallStep.Pending -> stringResource(MR.strings.ext_pending)
|
modifier: Modifier = Modifier,
|
||||||
InstallStep.Downloading -> stringResource(MR.strings.ext_downloading)
|
) {
|
||||||
InstallStep.Installing -> stringResource(MR.strings.ext_installing)
|
Box(
|
||||||
else -> error("Must not show non-install process text")
|
modifier = modifier.size(48.dp),
|
||||||
},
|
contentAlignment = Alignment.Center,
|
||||||
)
|
) {
|
||||||
}
|
val idle = installStep.isCompleted()
|
||||||
}
|
if (!idle) {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
modifier = Modifier.size(48.dp),
|
||||||
|
strokeWidth = 2.dp,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val padding by animateDpAsState(
|
||||||
|
targetValue = if (idle) 0.dp else 8.dp,
|
||||||
|
label = "iconPadding",
|
||||||
|
)
|
||||||
|
ExtensionIcon(
|
||||||
|
extension = extension,
|
||||||
|
modifier = Modifier
|
||||||
|
.matchParentSize()
|
||||||
|
.padding(padding),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plain text for what an extension simply is, pills for the few things worth noticing about it.
|
||||||
|
* Won't look good when it wraps, but it's not like overflowing content can be ellipsized.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
private fun ExtensionItemMetadata(
|
||||||
|
extension: Extension,
|
||||||
|
installStep: InstallStep,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
FlowRow(
|
||||||
|
modifier = modifier,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall / 2),
|
||||||
|
itemVerticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
val facts = listOfNotNull(
|
||||||
|
(extension as? Extension.Loaded)
|
||||||
|
?.lang
|
||||||
|
?.takeIf { it.isNotEmpty() }
|
||||||
|
?.let { LocaleHelper.getSourceDisplayName(it, LocalContext.current) },
|
||||||
|
extension.versionName.takeIf { it.isNotEmpty() },
|
||||||
|
)
|
||||||
|
if (facts.isNotEmpty()) {
|
||||||
|
Text(
|
||||||
|
text = facts.joinToString(" \u2022 "),
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val notable = listOfNotNull(
|
||||||
|
when (installStep) {
|
||||||
|
InstallStep.Pending -> MR.strings.ext_pending
|
||||||
|
InstallStep.Downloading -> MR.strings.ext_downloading
|
||||||
|
InstallStep.Installing -> MR.strings.ext_installing
|
||||||
|
else -> null
|
||||||
|
}?.let { it to MaterialTheme.colorScheme.primary },
|
||||||
|
when {
|
||||||
|
extension is Extension.NotLoaded ->
|
||||||
|
extension.reason.labelRes?.let { it to MaterialTheme.colorScheme.error }
|
||||||
|
extension is Extension.Loaded && extension.isObsolete ->
|
||||||
|
MR.strings.ext_obsolete to MaterialTheme.colorScheme.error
|
||||||
|
else -> null
|
||||||
|
},
|
||||||
|
extension.contentWarning.label?.let { it.title to it.color },
|
||||||
|
(extension as? Extension.Loaded)
|
||||||
|
?.takeIf { !it.isShared }
|
||||||
|
?.let { MR.strings.ext_installer_private to MaterialTheme.colorScheme.onSurfaceVariant },
|
||||||
|
)
|
||||||
|
notable.forEach { (label, color) ->
|
||||||
|
ExtensionPill(text = stringResource(label), color = color)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -417,76 +460,50 @@ private fun ExtensionItemActions(
|
|||||||
onClickItemAction: (Extension) -> Unit = {},
|
onClickItemAction: (Extension) -> Unit = {},
|
||||||
onClickItemSecondaryAction: (Extension) -> Unit = {},
|
onClickItemSecondaryAction: (Extension) -> Unit = {},
|
||||||
) {
|
) {
|
||||||
val isIdle = installStep.isCompleted()
|
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
when {
|
if (!installStep.isCompleted()) {
|
||||||
!isIdle -> {
|
IconButton(onClick = { onClickItemCancel(extension) }) {
|
||||||
IconButton(onClick = { onClickItemCancel(extension) }) {
|
Icon(
|
||||||
Icon(
|
imageVector = MaterialSymbols.Rounded.Close,
|
||||||
imageVector = Icons.Outlined.Close,
|
contentDescription = stringResource(MR.strings.action_cancel),
|
||||||
contentDescription = stringResource(MR.strings.action_cancel),
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
installStep == InstallStep.Error -> {
|
return@Row
|
||||||
IconButton(onClick = { onClickItemAction(extension) }) {
|
}
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Outlined.Refresh,
|
val secondaryAction = when (extension) {
|
||||||
contentDescription = stringResource(MR.strings.action_retry),
|
is Extension.Available -> {
|
||||||
)
|
(MaterialSymbols.Rounded.Public to MR.strings.action_open_in_web_view)
|
||||||
}
|
.takeIf { extension.sources.isNotEmpty() }
|
||||||
}
|
}
|
||||||
installStep == InstallStep.Idle -> {
|
is Extension.Loaded -> MaterialSymbols.Rounded.Settings to MR.strings.action_settings
|
||||||
when (extension) {
|
is Extension.NotLoaded -> if (extension.reason is Extension.NotLoaded.Reason.Untrusted) {
|
||||||
is Extension.Installed -> {
|
MaterialSymbols.Rounded.VerifiedUser to MR.strings.ext_trust
|
||||||
IconButton(onClick = { onClickItemSecondaryAction(extension) }) {
|
} else {
|
||||||
Icon(
|
MaterialSymbols.Rounded.Info to MR.strings.ext_not_loaded_details
|
||||||
imageVector = Icons.Outlined.Settings,
|
}
|
||||||
contentDescription = stringResource(MR.strings.action_settings),
|
}
|
||||||
)
|
secondaryAction?.let { (icon, label) ->
|
||||||
}
|
IconButton(onClick = { onClickItemSecondaryAction(extension) }) {
|
||||||
|
Icon(imageVector = icon, contentDescription = stringResource(label))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (extension.hasUpdate) {
|
val action = when {
|
||||||
IconButton(onClick = { onClickItemAction(extension) }) {
|
installStep == InstallStep.Error -> MaterialSymbols.Rounded.Refresh to MR.strings.action_retry
|
||||||
Icon(
|
extension is Extension.Available -> MaterialSymbols.Rounded.Download to MR.strings.ext_install
|
||||||
imageVector = Icons.Outlined.GetApp,
|
extension is Extension.Installed && extension.hasUpdate -> {
|
||||||
contentDescription = stringResource(MR.strings.ext_update),
|
MaterialSymbols.Rounded.Download to MR.strings.ext_update
|
||||||
)
|
}
|
||||||
}
|
else -> null
|
||||||
}
|
}
|
||||||
}
|
action?.let { (icon, label) ->
|
||||||
is Extension.Untrusted -> {
|
IconButton(onClick = { onClickItemAction(extension) }) {
|
||||||
IconButton(onClick = { onClickItemAction(extension) }) {
|
Icon(imageVector = icon, contentDescription = stringResource(label))
|
||||||
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),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -526,6 +543,83 @@ private fun ExtensionHeader(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Only the reasons the user can act on are worth naming in the row; the rest all mean "broken" to
|
||||||
|
* them and are spelled out in [ExtensionNotLoadedDialog] instead.
|
||||||
|
*/
|
||||||
|
private val Extension.NotLoaded.Reason.labelRes: StringResource?
|
||||||
|
get() = when (this) {
|
||||||
|
is Extension.NotLoaded.Reason.Untrusted -> MR.strings.ext_untrusted
|
||||||
|
Extension.NotLoaded.Reason.Filtered -> MR.strings.ext_filtered
|
||||||
|
// The section header already says these aren't loaded; the dialog says why
|
||||||
|
Extension.NotLoaded.Reason.Unsigned,
|
||||||
|
Extension.NotLoaded.Reason.UnsupportedLibVersion,
|
||||||
|
Extension.NotLoaded.Reason.Malformed,
|
||||||
|
is Extension.NotLoaded.Reason.Failed,
|
||||||
|
-> null
|
||||||
|
}
|
||||||
|
|
||||||
|
private val Extension.NotLoaded.Reason.descriptionRes: StringResource
|
||||||
|
get() = when (this) {
|
||||||
|
is Extension.NotLoaded.Reason.Untrusted -> MR.strings.untrusted_extension_message
|
||||||
|
Extension.NotLoaded.Reason.Filtered -> MR.strings.ext_filtered_message
|
||||||
|
Extension.NotLoaded.Reason.Unsigned -> MR.strings.ext_unsigned_message
|
||||||
|
Extension.NotLoaded.Reason.UnsupportedLibVersion -> MR.strings.ext_unsupported_message
|
||||||
|
Extension.NotLoaded.Reason.Malformed -> MR.strings.ext_malformed_message
|
||||||
|
is Extension.NotLoaded.Reason.Failed -> MR.strings.ext_load_failed_message
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ExtensionNotLoadedDialog(
|
||||||
|
reason: Extension.NotLoaded.Reason,
|
||||||
|
onClickUninstall: () -> Unit,
|
||||||
|
onDismissRequest: () -> Unit,
|
||||||
|
) {
|
||||||
|
AlertDialog(
|
||||||
|
title = {
|
||||||
|
Text(text = stringResource(MR.strings.ext_not_loaded_dialog))
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small)) {
|
||||||
|
Text(text = stringResource(reason.descriptionRes))
|
||||||
|
|
||||||
|
if (reason is Extension.NotLoaded.Reason.Failed) {
|
||||||
|
Text(
|
||||||
|
text = reason.message,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
fontFamily = FontFamily.Monospace,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
|
||||||
|
val context = LocalContext.current
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
context.copyToClipboard(
|
||||||
|
label = context.stringResource(MR.strings.ext_copy_stacktrace),
|
||||||
|
content = reason.stackTrace,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
contentPadding = PaddingValues(0.dp),
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(MR.strings.ext_copy_stacktrace))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(onClick = onDismissRequest) {
|
||||||
|
Text(text = stringResource(MR.strings.action_ok))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = onClickUninstall) {
|
||||||
|
Text(text = stringResource(MR.strings.ext_uninstall))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ExtensionTrustDialog(
|
private fun ExtensionTrustDialog(
|
||||||
onClickConfirm: () -> Unit,
|
onClickConfirm: () -> Unit,
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ import eu.kanade.presentation.browse.components.GlobalSearchErrorResultItem
|
|||||||
import eu.kanade.presentation.browse.components.GlobalSearchLoadingResultItem
|
import eu.kanade.presentation.browse.components.GlobalSearchLoadingResultItem
|
||||||
import eu.kanade.presentation.browse.components.GlobalSearchResultItem
|
import eu.kanade.presentation.browse.components.GlobalSearchResultItem
|
||||||
import eu.kanade.presentation.browse.components.GlobalSearchToolbar
|
import eu.kanade.presentation.browse.components.GlobalSearchToolbar
|
||||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchItemResult
|
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchItemResult
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchScreenModel
|
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchViewModel
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SourceFilter
|
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SourceFilter
|
||||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
@@ -20,14 +20,14 @@ import tachiyomi.presentation.core.components.material.Scaffold
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun GlobalSearchScreen(
|
fun GlobalSearchScreen(
|
||||||
state: SearchScreenModel.State,
|
state: SearchViewModel.State,
|
||||||
navigateUp: () -> Unit,
|
navigateUp: () -> Unit,
|
||||||
onChangeSearchQuery: (String?) -> Unit,
|
onChangeSearchQuery: (String?) -> Unit,
|
||||||
onSearch: (String) -> Unit,
|
onSearch: (String) -> Unit,
|
||||||
onChangeSearchFilter: (SourceFilter) -> Unit,
|
onChangeSearchFilter: (SourceFilter) -> Unit,
|
||||||
onToggleResults: () -> Unit,
|
onToggleResults: () -> Unit,
|
||||||
getManga: @Composable (Manga) -> State<Manga>,
|
getManga: @Composable (Manga) -> State<Manga>,
|
||||||
onClickSource: (CatalogueSource) -> Unit,
|
onClickSource: (Source) -> Unit,
|
||||||
onClickItem: (Manga) -> Unit,
|
onClickItem: (Manga) -> Unit,
|
||||||
onLongClickItem: (Manga) -> Unit,
|
onLongClickItem: (Manga) -> Unit,
|
||||||
) {
|
) {
|
||||||
@@ -62,10 +62,10 @@ fun GlobalSearchScreen(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun GlobalSearchContent(
|
internal fun GlobalSearchContent(
|
||||||
items: Map<CatalogueSource, SearchItemResult>,
|
items: Map<Source, SearchItemResult>,
|
||||||
contentPadding: PaddingValues,
|
contentPadding: PaddingValues,
|
||||||
getManga: @Composable (Manga) -> State<Manga>,
|
getManga: @Composable (Manga) -> State<Manga>,
|
||||||
onClickSource: (CatalogueSource) -> Unit,
|
onClickSource: (Source) -> Unit,
|
||||||
onClickItem: (Manga) -> Unit,
|
onClickItem: (Manga) -> Unit,
|
||||||
onLongClickItem: (Manga) -> Unit,
|
onLongClickItem: (Manga) -> Unit,
|
||||||
fromSourceId: Long? = null,
|
fromSourceId: Long? = null,
|
||||||
|
|||||||
@@ -3,15 +3,15 @@ package eu.kanade.presentation.browse
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.State
|
import androidx.compose.runtime.State
|
||||||
import eu.kanade.presentation.browse.components.GlobalSearchToolbar
|
import eu.kanade.presentation.browse.components.GlobalSearchToolbar
|
||||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchScreenModel
|
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchViewModel
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SourceFilter
|
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SourceFilter
|
||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
import tachiyomi.presentation.core.components.material.Scaffold
|
import tachiyomi.presentation.core.components.material.Scaffold
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MigrateSearchScreen(
|
fun MigrateSearchScreen(
|
||||||
state: SearchScreenModel.State,
|
state: SearchViewModel.State,
|
||||||
fromSourceId: Long?,
|
fromSourceId: Long?,
|
||||||
navigateUp: () -> Unit,
|
navigateUp: () -> Unit,
|
||||||
onChangeSearchQuery: (String?) -> Unit,
|
onChangeSearchQuery: (String?) -> Unit,
|
||||||
@@ -19,7 +19,7 @@ fun MigrateSearchScreen(
|
|||||||
onChangeSearchFilter: (SourceFilter) -> Unit,
|
onChangeSearchFilter: (SourceFilter) -> Unit,
|
||||||
onToggleResults: () -> Unit,
|
onToggleResults: () -> Unit,
|
||||||
getManga: @Composable (Manga) -> State<Manga>,
|
getManga: @Composable (Manga) -> State<Manga>,
|
||||||
onClickSource: (CatalogueSource) -> Unit,
|
onClickSource: (Source) -> Unit,
|
||||||
onClickItem: (Manga) -> Unit,
|
onClickItem: (Manga) -> Unit,
|
||||||
onLongClickItem: (Manga) -> Unit,
|
onLongClickItem: (Manga) -> Unit,
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -6,12 +6,8 @@ import androidx.compose.foundation.layout.Column
|
|||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.outlined.ArrowDownward
|
|
||||||
import androidx.compose.material.icons.outlined.ArrowUpward
|
|
||||||
import androidx.compose.material.icons.outlined.Numbers
|
|
||||||
import androidx.compose.material.icons.outlined.SortByAlpha
|
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
@@ -21,12 +17,17 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
import eu.kanade.domain.source.interactor.SetMigrateSorting
|
import eu.kanade.domain.source.interactor.SetMigrateSorting
|
||||||
import eu.kanade.presentation.browse.components.BaseSourceItem
|
import eu.kanade.presentation.browse.components.BaseSourceItem
|
||||||
import eu.kanade.presentation.browse.components.SourceIcon
|
import eu.kanade.presentation.browse.components.SourceIcon
|
||||||
import eu.kanade.tachiyomi.ui.browse.migration.sources.MigrateSourceScreenModel
|
import eu.kanade.tachiyomi.ui.browse.migration.sources.MigrateSourceViewModel
|
||||||
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import mihon.icons.materialsymbols.MaterialSymbols
|
||||||
|
import mihon.icons.materialsymbols.rounded.ArrowDownward
|
||||||
|
import mihon.icons.materialsymbols.rounded.ArrowUpward
|
||||||
|
import mihon.icons.materialsymbols.rounded.Numbers
|
||||||
|
import mihon.icons.materialsymbols.rounded.SortByAlpha
|
||||||
import tachiyomi.domain.source.model.Source
|
import tachiyomi.domain.source.model.Source
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.components.Badge
|
import tachiyomi.presentation.core.components.Badge
|
||||||
@@ -44,7 +45,7 @@ import tachiyomi.presentation.core.util.secondaryItemAlpha
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MigrateSourceScreen(
|
fun MigrateSourceScreen(
|
||||||
state: MigrateSourceScreenModel.State,
|
state: MigrateSourceViewModel.State,
|
||||||
contentPadding: PaddingValues,
|
contentPadding: PaddingValues,
|
||||||
onClickItem: (Source) -> Unit,
|
onClickItem: (Source) -> Unit,
|
||||||
onToggleSortingDirection: () -> Unit,
|
onToggleSortingDirection: () -> Unit,
|
||||||
@@ -76,7 +77,7 @@ fun MigrateSourceScreen(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun MigrateSourceList(
|
private fun MigrateSourceList(
|
||||||
list: ImmutableList<Pair<Source, Long>>,
|
list: List<Pair<Source, Long>>,
|
||||||
contentPadding: PaddingValues,
|
contentPadding: PaddingValues,
|
||||||
onClickItem: (Source) -> Unit,
|
onClickItem: (Source) -> Unit,
|
||||||
onLongClickItem: (Source) -> Unit,
|
onLongClickItem: (Source) -> Unit,
|
||||||
@@ -104,11 +105,11 @@ private fun MigrateSourceList(
|
|||||||
IconButton(onClick = onToggleSortingMode) {
|
IconButton(onClick = onToggleSortingMode) {
|
||||||
when (sortingMode) {
|
when (sortingMode) {
|
||||||
SetMigrateSorting.Mode.ALPHABETICAL -> Icon(
|
SetMigrateSorting.Mode.ALPHABETICAL -> Icon(
|
||||||
Icons.Outlined.SortByAlpha,
|
MaterialSymbols.Rounded.SortByAlpha,
|
||||||
contentDescription = stringResource(MR.strings.action_sort_alpha),
|
contentDescription = stringResource(MR.strings.action_sort_alpha),
|
||||||
)
|
)
|
||||||
SetMigrateSorting.Mode.TOTAL -> Icon(
|
SetMigrateSorting.Mode.TOTAL -> Icon(
|
||||||
Icons.Outlined.Numbers,
|
MaterialSymbols.Rounded.Numbers,
|
||||||
contentDescription = stringResource(MR.strings.action_sort_count),
|
contentDescription = stringResource(MR.strings.action_sort_count),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -116,11 +117,11 @@ private fun MigrateSourceList(
|
|||||||
IconButton(onClick = onToggleSortingDirection) {
|
IconButton(onClick = onToggleSortingDirection) {
|
||||||
when (sortingDirection) {
|
when (sortingDirection) {
|
||||||
SetMigrateSorting.Direction.ASCENDING -> Icon(
|
SetMigrateSorting.Direction.ASCENDING -> Icon(
|
||||||
Icons.Outlined.ArrowUpward,
|
MaterialSymbols.Rounded.ArrowUpward,
|
||||||
contentDescription = stringResource(MR.strings.action_asc),
|
contentDescription = stringResource(MR.strings.action_asc),
|
||||||
)
|
)
|
||||||
SetMigrateSorting.Direction.DESCENDING -> Icon(
|
SetMigrateSorting.Direction.DESCENDING -> Icon(
|
||||||
Icons.Outlined.ArrowDownward,
|
MaterialSymbols.Rounded.ArrowDownward,
|
||||||
contentDescription = stringResource(MR.strings.action_desc),
|
contentDescription = stringResource(MR.strings.action_desc),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -157,7 +158,7 @@ private fun MigrateSourceItem(
|
|||||||
showLanguageInContent = source.lang != "",
|
showLanguageInContent = source.lang != "",
|
||||||
onClickItem = onClickItem,
|
onClickItem = onClickItem,
|
||||||
onLongClickItem = onLongClickItem,
|
onLongClickItem = onLongClickItem,
|
||||||
icon = { SourceIcon(source = source) },
|
icon = { SourceIcon(source = source, modifier = Modifier.size(48.dp)) },
|
||||||
action = {
|
action = {
|
||||||
BadgeGroup {
|
BadgeGroup {
|
||||||
Badge(text = "$count")
|
Badge(text = "$count")
|
||||||
|
|||||||
@@ -10,8 +10,7 @@ import androidx.compose.ui.platform.LocalContext
|
|||||||
import eu.kanade.presentation.browse.components.BaseSourceItem
|
import eu.kanade.presentation.browse.components.BaseSourceItem
|
||||||
import eu.kanade.presentation.components.AppBar
|
import eu.kanade.presentation.components.AppBar
|
||||||
import eu.kanade.presentation.more.settings.widget.SwitchPreferenceWidget
|
import eu.kanade.presentation.more.settings.widget.SwitchPreferenceWidget
|
||||||
import eu.kanade.presentation.util.animateItemFastScroll
|
import eu.kanade.tachiyomi.ui.browse.source.SourcesFilterViewModel
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.SourcesFilterScreenModel
|
|
||||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||||
import tachiyomi.domain.source.model.Source
|
import tachiyomi.domain.source.model.Source
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
@@ -23,7 +22,7 @@ import tachiyomi.presentation.core.screens.EmptyScreen
|
|||||||
@Composable
|
@Composable
|
||||||
fun SourcesFilterScreen(
|
fun SourcesFilterScreen(
|
||||||
navigateUp: () -> Unit,
|
navigateUp: () -> Unit,
|
||||||
state: SourcesFilterScreenModel.State.Success,
|
state: SourcesFilterViewModel.State.Success,
|
||||||
onClickLanguage: (String) -> Unit,
|
onClickLanguage: (String) -> Unit,
|
||||||
onClickSource: (Source) -> Unit,
|
onClickSource: (Source) -> Unit,
|
||||||
) {
|
) {
|
||||||
@@ -55,7 +54,7 @@ fun SourcesFilterScreen(
|
|||||||
@Composable
|
@Composable
|
||||||
private fun SourcesFilterContent(
|
private fun SourcesFilterContent(
|
||||||
contentPadding: PaddingValues,
|
contentPadding: PaddingValues,
|
||||||
state: SourcesFilterScreenModel.State.Success,
|
state: SourcesFilterViewModel.State.Success,
|
||||||
onClickLanguage: (String) -> Unit,
|
onClickLanguage: (String) -> Unit,
|
||||||
onClickSource: (Source) -> Unit,
|
onClickSource: (Source) -> Unit,
|
||||||
) {
|
) {
|
||||||
@@ -69,7 +68,7 @@ private fun SourcesFilterContent(
|
|||||||
contentType = "source-filter-header",
|
contentType = "source-filter-header",
|
||||||
) {
|
) {
|
||||||
SourcesFilterHeader(
|
SourcesFilterHeader(
|
||||||
modifier = Modifier.animateItemFastScroll(),
|
modifier = Modifier.animateItem(),
|
||||||
language = language,
|
language = language,
|
||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
onClickItem = onClickLanguage,
|
onClickItem = onClickLanguage,
|
||||||
@@ -82,7 +81,7 @@ private fun SourcesFilterContent(
|
|||||||
contentType = { "source-filter-item" },
|
contentType = { "source-filter-item" },
|
||||||
) { source ->
|
) { source ->
|
||||||
SourcesFilterItem(
|
SourcesFilterItem(
|
||||||
modifier = Modifier.animateItemFastScroll(),
|
modifier = Modifier.animateItem(),
|
||||||
source = source,
|
source = source,
|
||||||
enabled = "${source.id}" !in state.disabledSources,
|
enabled = "${source.id}" !in state.disabledSources,
|
||||||
onClickItem = onClickSource,
|
onClickItem = onClickSource,
|
||||||
|
|||||||
@@ -6,9 +6,6 @@ import androidx.compose.foundation.layout.PaddingValues
|
|||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.filled.PushPin
|
|
||||||
import androidx.compose.material.icons.outlined.PushPin
|
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
@@ -21,9 +18,12 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import eu.kanade.presentation.browse.components.BaseSourceItem
|
import eu.kanade.presentation.browse.components.BaseSourceItem
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.SourcesScreenModel
|
import eu.kanade.tachiyomi.ui.browse.source.SourcesViewModel
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreenModel.Listing
|
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceViewModel.Listing
|
||||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||||
|
import mihon.icons.materialsymbols.MaterialSymbols
|
||||||
|
import mihon.icons.materialsymbols.rounded.Keep
|
||||||
|
import mihon.icons.materialsymbols.roundedfilled.Keep
|
||||||
import tachiyomi.domain.source.model.Pin
|
import tachiyomi.domain.source.model.Pin
|
||||||
import tachiyomi.domain.source.model.Source
|
import tachiyomi.domain.source.model.Source
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
@@ -40,7 +40,7 @@ import tachiyomi.source.local.isLocal
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SourcesScreen(
|
fun SourcesScreen(
|
||||||
state: SourcesScreenModel.State,
|
state: SourcesViewModel.State,
|
||||||
contentPadding: PaddingValues,
|
contentPadding: PaddingValues,
|
||||||
onClickItem: (Source, Listing) -> Unit,
|
onClickItem: (Source, Listing) -> Unit,
|
||||||
onClickPin: (Source) -> Unit,
|
onClickPin: (Source) -> Unit,
|
||||||
@@ -143,7 +143,7 @@ private fun SourcePinButton(
|
|||||||
isPinned: Boolean,
|
isPinned: Boolean,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
) {
|
) {
|
||||||
val icon = if (isPinned) Icons.Filled.PushPin else Icons.Outlined.PushPin
|
val icon = if (isPinned) MaterialSymbols.RoundedFilled.Keep else MaterialSymbols.Rounded.Keep
|
||||||
val tint = if (isPinned) {
|
val tint = if (isPinned) {
|
||||||
MaterialTheme.colorScheme.primary
|
MaterialTheme.colorScheme.primary
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -3,12 +3,14 @@ package eu.kanade.presentation.browse.components
|
|||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.RowScope
|
import androidx.compose.foundation.layout.RowScope
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||||
import tachiyomi.domain.source.model.Source
|
import tachiyomi.domain.source.model.Source
|
||||||
import tachiyomi.presentation.core.components.material.padding
|
import tachiyomi.presentation.core.components.material.padding
|
||||||
@@ -39,7 +41,7 @@ fun BaseSourceItem(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private val defaultIcon: @Composable RowScope.(Source) -> Unit = { source ->
|
private val defaultIcon: @Composable RowScope.(Source) -> Unit = { source ->
|
||||||
SourceIcon(source = source)
|
SourceIcon(source = source, modifier = Modifier.size(48.dp))
|
||||||
}
|
}
|
||||||
|
|
||||||
private val defaultContent: @Composable RowScope.(Source, String?) -> Unit = { source, sourceLangString ->
|
private val defaultContent: @Composable RowScope.(Source, String?) -> Unit = { source, sourceLangString ->
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
package eu.kanade.presentation.browse.components
|
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 androidx.compose.runtime.Composable
|
||||||
|
import mihon.icons.materialsymbols.MaterialSymbols
|
||||||
|
import mihon.icons.materialsymbols.rounded.CollectionsBookmark
|
||||||
import tachiyomi.presentation.core.components.Badge
|
import tachiyomi.presentation.core.components.Badge
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun InLibraryBadge(enabled: Boolean) {
|
internal fun InLibraryBadge(enabled: Boolean) {
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
Badge(
|
Badge(
|
||||||
imageVector = Icons.Outlined.CollectionsBookmark,
|
imageVector = MaterialSymbols.Rounded.CollectionsBookmark,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user