Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dd2949638c | ||
|
|
f71aa4902b | ||
|
|
af7e411932 | ||
|
|
c54ac1a4c0 | ||
|
|
00d6a2e403 | ||
|
|
b67d0938b0 | ||
|
|
8447760cb5 | ||
|
|
24d2072586 | ||
|
|
a52ab28534 | ||
|
|
601713d8d7 | ||
|
|
7bfa5be53c | ||
|
|
ae3a66903a | ||
|
|
5438fd5d9b | ||
|
|
a74ffca03d | ||
|
|
c5a83d567e | ||
|
|
1b1f781dd9 | ||
|
|
988195e4fc | ||
|
|
4d4124458d | ||
|
|
0ae2843676 | ||
|
|
beaddc9eeb | ||
|
|
d843b3a14b | ||
|
|
b9da0eda8b | ||
|
|
ba75cda036 | ||
|
|
9d59af29c5 | ||
|
|
660d6e32f1 | ||
|
|
c07c663105 | ||
|
|
3673f6f7f9 | ||
|
|
e8120e12d0 | ||
|
|
f7459886b2 | ||
|
|
6fe5319c08 | ||
|
|
3a42208a36 | ||
|
|
e4eacce5bf | ||
|
|
d1fdea8d3e | ||
|
|
98e452e39e | ||
|
|
02e8fcc534 | ||
|
|
70852c2725 | ||
|
|
ea67d6e078 | ||
|
|
d2aa3b5347 | ||
|
|
937792ba91 | ||
|
|
fcfdaf59f8 | ||
|
|
0d4da998a1 | ||
|
|
0e7a32d9af | ||
|
|
4bb6b0baf6 | ||
|
|
0591b3224f | ||
|
|
eaf8dd21ff | ||
|
|
9bc6cccf26 | ||
|
|
eb3b53ee5b | ||
|
|
731af586af | ||
|
|
3acb70b8e1 | ||
|
|
836b1841e3 | ||
|
|
d37f4580a9 | ||
|
|
70fc964b4d | ||
|
|
c3c5fbfc79 | ||
|
|
04103f9280 | ||
|
|
69370efcf5 | ||
|
|
f0d3b0c8ae | ||
|
|
370c485048 | ||
|
|
7e4c910cd7 | ||
|
|
e8c1c80ce5 | ||
|
|
0d55f319c9 | ||
|
|
42ed51094e | ||
|
|
84c3fee5da | ||
|
|
b3af2c9481 | ||
|
|
33cab186fd | ||
|
|
e3f52412ee | ||
|
|
080810c707 | ||
|
|
964e827cbe | ||
|
|
cae02acfc4 | ||
|
|
31ce4eac92 | ||
|
|
bd5bd1e571 | ||
|
|
2b4a6dc57a | ||
|
|
68932731b9 | ||
|
|
aab7ba0d53 | ||
|
|
59bf4f2702 | ||
|
|
e0e4b739f9 | ||
|
|
21d6b3bbb8 | ||
|
|
dae5d67515 | ||
|
|
a6a814bbbb | ||
|
|
0b0555af9d | ||
|
|
6e0d77a8ab | ||
|
|
5c06a69c8a | ||
|
|
188a236a90 | ||
|
|
63ec1aa217 | ||
|
|
829316d8b9 | ||
|
|
337739fdad | ||
|
|
f144ac701d | ||
|
|
9c9b8b74cd | ||
|
|
154c7f3710 | ||
|
|
d3308bc987 | ||
|
|
04d9ee57d7 | ||
|
|
f3eecf18fb | ||
|
|
6591495d2d | ||
|
|
2497835628 | ||
|
|
8fad9b2abe | ||
|
|
fdd0ec9fd2 | ||
|
|
1219ff99ff | ||
|
|
c60b14998a | ||
|
|
380554bd19 | ||
|
|
2b969030da | ||
|
|
da917de1f4 | ||
|
|
541f9e89f3 | ||
|
|
f4a987c83f | ||
|
|
37474ed6ac | ||
|
|
cb69a168f3 | ||
|
|
94366553de | ||
|
|
383332bfbb | ||
|
|
4bb1a17d39 | ||
|
|
94d68fe38b | ||
|
|
cf6c850d2a | ||
|
|
c8a3a0fc20 | ||
|
|
433896b267 | ||
|
|
7e9c1f00ef | ||
|
|
2b5b0d695a | ||
|
|
2265b0943e | ||
|
|
78f12065ab | ||
|
|
f28c45a13a | ||
|
|
e030500917 | ||
|
|
9c042cd323 | ||
|
|
44eea7a273 | ||
|
|
834cfc5e8c | ||
|
|
876bb68873 | ||
|
|
40c1b7ef21 | ||
|
|
6191ad74d3 | ||
|
|
5aae8f5649 | ||
|
|
6886465afb | ||
|
|
5a46fc4573 | ||
|
|
a08b8e96e5 | ||
|
|
5aeded7a24 | ||
|
|
1883b82159 | ||
|
|
302b57d4ac | ||
|
|
c6b793923e | ||
|
|
058c280fd7 | ||
|
|
0fa42d731b | ||
|
|
9b54b57934 | ||
|
|
d3ab3ba61f | ||
|
|
921f570151 | ||
|
|
03ab435401 | ||
|
|
b73704ede1 | ||
|
|
97b2181ff7 | ||
|
|
ef5e3f7c51 | ||
|
|
8a29d46b38 | ||
|
|
75d8ff4b64 | ||
|
|
33258ea554 | ||
|
|
a723b4ecd3 | ||
|
|
78a237fb0e | ||
|
|
8fe60c0163 | ||
|
|
ffe47b2ff2 | ||
|
|
e05fffb41c | ||
|
|
1fb5de2527 | ||
|
|
46790a1fbe | ||
|
|
af1316f06b | ||
|
|
24564cac8d | ||
|
|
c4868f137b | ||
|
|
3ccfa0dd5b | ||
|
|
fded0fd08d | ||
|
|
b1afe9bcd1 | ||
|
|
22b5a175f4 | ||
|
|
ee9e0df12e | ||
|
|
dd525baf88 | ||
|
|
9180603ead | ||
|
|
66fe19369e | ||
|
|
b9db025f57 | ||
|
|
44f6eca3aa | ||
|
|
1ff1caf421 | ||
|
|
ec1355c0e7 | ||
|
|
d23a453f71 | ||
|
|
94b138f254 | ||
|
|
c281daa4ea | ||
|
|
4f9eba8c63 | ||
|
|
a2547f1e8d | ||
|
|
abcef9a86e | ||
|
|
7d6a9b10e4 | ||
|
|
9322b80790 | ||
|
|
e12897bd13 | ||
|
|
28279cc671 | ||
|
|
2531fa289b | ||
|
|
9673163e8f | ||
|
|
3f7091f880 | ||
|
|
5df75fd534 | ||
|
|
62061e54ee | ||
|
|
436c07fb6b | ||
|
|
a16f873f7d | ||
|
|
98fdcf0588 | ||
|
|
539ad1af30 | ||
|
|
4370606977 | ||
|
|
1ecdc07655 | ||
|
|
01a1113a07 | ||
|
|
fd6c2977b0 | ||
|
|
02e36e7021 | ||
|
|
7f8e4aa003 | ||
|
|
8ce16bc918 | ||
|
|
fbe35a0fa0 | ||
|
|
931f1ec514 | ||
|
|
7c4a59e9a4 | ||
|
|
c1c6f25763 | ||
|
|
e7f515c70f | ||
|
|
64fecbfa52 | ||
|
|
c6c181a9fa | ||
|
|
53a1bd9959 | ||
|
|
7b1f94cf5a | ||
|
|
4b8ecbf6a7 | ||
|
|
0e0b0519e7 | ||
|
|
7fb3b7c785 | ||
|
|
e18ebb1feb | ||
|
|
f76dcfc97b | ||
|
|
c0c630bd9f | ||
|
|
04df7da5dc | ||
|
|
d156638a79 | ||
|
|
c78fe0b72c | ||
|
|
799c37ea71 | ||
|
|
9559ca571b | ||
|
|
2f5c1e11c6 | ||
|
|
734c73fa4d | ||
|
|
c09173d62c | ||
|
|
533f50ed08 | ||
|
|
b4422fe910 | ||
|
|
22e58a034f | ||
|
|
784856d341 | ||
|
|
f82832ca36 | ||
|
|
6a70aa792e | ||
|
|
a0016dbf85 | ||
|
|
b982705b2f | ||
|
|
4c36398bed | ||
|
|
7e1b3abcf6 | ||
|
|
97bb65aa35 | ||
|
|
98c1a377ca | ||
|
|
2d2ce3281e | ||
|
|
a28d3ba840 | ||
|
|
ab2e4a8e2d | ||
|
|
67ddba87b5 | ||
|
|
8164847fcb | ||
|
|
85b538bbd3 | ||
|
|
d00d700a4b | ||
|
|
ae66318843 | ||
|
|
1b9aa6a762 | ||
|
|
306ce83de4 | ||
|
|
89f9735ab9 | ||
|
|
843e1cc6a0 | ||
|
|
1a53d0d207 | ||
|
|
45c02d4fd6 | ||
|
|
862dee0845 | ||
|
|
809bbfeba7 | ||
|
|
959c740175 | ||
|
|
a718510c89 | ||
|
|
f7c2fd8af3 | ||
|
|
c76440f167 | ||
|
|
c3bd959ee5 | ||
|
|
0a25643b87 | ||
|
|
c805ca8aff | ||
|
|
699a1ab801 | ||
|
|
ee091aee0f | ||
|
|
673c1837e9 | ||
|
|
3998913c17 | ||
|
|
1e83d7128e | ||
|
|
355289a576 | ||
|
|
ba3a41f0ee | ||
|
|
fc6d0bd6ef | ||
|
|
15ef0ae362 | ||
|
|
a598f8562d | ||
|
|
2860fed5ab | ||
|
|
f247544439 | ||
|
|
fdc3d94d5c | ||
|
|
afc2ff7478 | ||
|
|
83072d4eee | ||
|
|
7e7be8d40a | ||
|
|
90d7555333 | ||
|
|
ba268cfe6d | ||
|
|
0a7a9beb84 | ||
|
|
b18b89db29 | ||
|
|
cb2d99bdc1 | ||
|
|
4dd6d68163 | ||
|
|
28ecc9d479 | ||
|
|
c917117db3 | ||
|
|
5c0d65dd60 | ||
|
|
dfc59aa643 | ||
|
|
6ab77e541f | ||
|
|
6224f8f4ab | ||
|
|
3d269c4220 | ||
|
|
176a802e96 | ||
|
|
417a06e140 | ||
|
|
5c4f034532 | ||
|
|
1d50855f52 | ||
|
|
0280450797 | ||
|
|
d0c907c7ff | ||
|
|
1302e17c01 | ||
|
|
33f83b7325 | ||
|
|
d2c49f5022 | ||
|
|
084883a308 | ||
|
|
67a73da720 | ||
|
|
f2447fd122 | ||
|
|
8ddcb003e0 | ||
|
|
0bff011f72 | ||
|
|
f41d1d7306 | ||
|
|
a30aa23b67 | ||
|
|
583d30eb5f | ||
|
|
c53b0986a9 | ||
|
|
85703c8c47 | ||
|
|
025ba0bf5b | ||
|
|
ac2def22d2 | ||
|
|
895cfc7bc6 | ||
|
|
49eb9d5077 | ||
|
|
5a3c165ff1 | ||
|
|
3946428df6 | ||
|
|
471d9784d6 | ||
|
|
0ba0afc0d0 | ||
|
|
5049b791fb | ||
|
|
9a3715c516 | ||
|
|
4ee276f636 | ||
|
|
d528b2d5d0 | ||
|
|
b3ee69d193 | ||
|
|
f882b78ff6 | ||
|
|
7e58101a3f | ||
|
|
5450eb9b2b | ||
|
|
69695e3c99 | ||
|
|
5b15059c04 | ||
|
|
f50d69cd60 | ||
|
|
df6f783a69 | ||
|
|
f0291d94f9 | ||
|
|
593f24b86f | ||
|
|
5bce898f99 | ||
|
|
2b444c262d | ||
|
|
e40a7c75d9 | ||
|
|
4a7c057260 | ||
|
|
229a00cd53 | ||
|
|
d2d0d880f1 | ||
|
|
c5e17c1d5b | ||
|
|
33ff2ebe06 | ||
|
|
5c13e188de | ||
|
|
60b03dcd0e | ||
|
|
4604a25f58 | ||
|
|
99029b8b04 | ||
|
|
70f46442c6 | ||
|
|
13cf42437f | ||
|
|
1e29f6dcb4 | ||
|
|
f25981792f | ||
|
|
934c8be3e4 | ||
|
|
b9994eacbc | ||
|
|
704ab0ee7e | ||
|
|
6d1d737f9f | ||
|
|
bc75156232 | ||
|
|
2101ea7a36 | ||
|
|
26921c8ae1 | ||
|
|
be1eecf3b5 | ||
|
|
2d4f154c9b | ||
|
|
d82cbe51ba | ||
|
|
d13ffa93ba | ||
|
|
2cce55953f | ||
|
|
07d100fcc0 | ||
|
|
cc04371181 | ||
|
|
ee50c88765 | ||
|
|
efc5b55ea7 | ||
|
|
94af66156a | ||
|
|
4861359f9e | ||
|
|
5af167dab4 | ||
|
|
db1f0bc6d7 | ||
|
|
b94a843ec5 | ||
|
|
b27d214f24 | ||
|
|
a81eb9d17b | ||
|
|
ec9b85f30f | ||
|
|
8ac2f33949 | ||
|
|
d84198ab28 | ||
|
|
34d339d9f1 | ||
|
|
bcfefc7c76 | ||
|
|
62539dc48e | ||
|
|
453ab11395 | ||
|
|
b2246b980a | ||
|
|
822a461acd | ||
|
|
25e5c0ab8e | ||
|
|
2a55db95f4 | ||
|
|
a144866182 | ||
|
|
be7a04a4cb | ||
|
|
fd1b861dab | ||
|
|
957ebd35aa | ||
|
|
29d8b11771 | ||
|
|
c87059e9d7 | ||
|
|
4babe2eec4 | ||
|
|
48fdaaca0c | ||
|
|
f0ee88b4d6 | ||
|
|
9210780b6b | ||
|
|
b479e28543 | ||
|
|
f93b98fc5e | ||
|
|
4721d1011b | ||
|
|
4da4443e67 | ||
|
|
e5343f4360 | ||
|
|
86323220dc | ||
|
|
ada8f3af75 | ||
|
|
fb940d3982 | ||
|
|
8e8729c25f | ||
|
|
ffdd063ca1 | ||
|
|
dfd5c6c70e | ||
|
|
3068aab681 | ||
|
|
e402587f8f | ||
|
|
00b1be879d | ||
|
|
e63bd49a4c | ||
|
|
8131170eb1 | ||
|
|
dc1abd3806 | ||
|
|
468fbd50da | ||
|
|
f2011f9b6f | ||
|
|
1e53a9b389 | ||
|
|
dc2504b67f | ||
|
|
2ae3e3b64c | ||
|
|
e6bdaf1172 | ||
|
|
36a66d9199 | ||
|
|
fb6fbb8df9 | ||
|
|
ef8449dc02 | ||
|
|
b59f8e9ade | ||
|
|
a49506f4a0 | ||
|
|
4ce3d25eed | ||
|
|
81b166bee1 | ||
|
|
37a587859a | ||
|
|
b7544630bb | ||
|
|
05c7a0fa39 | ||
|
|
eee7655f80 | ||
|
|
9390813c48 | ||
|
|
d032fbdd67 | ||
|
|
adcd73a590 | ||
|
|
87b405bf1f | ||
|
|
d8306e2c80 | ||
|
|
c03fe9b51e | ||
|
|
9099014d28 | ||
|
|
d7b3872486 | ||
|
|
4ae2af0bef | ||
|
|
c9bcd46b77 | ||
|
|
e531653634 | ||
|
|
a29e4c72b1 | ||
|
|
9e1f7ca051 | ||
|
|
fbbc347411 | ||
|
|
ea364536a4 | ||
|
|
575fc9e94c | ||
|
|
703c63125a | ||
|
|
b205b5e120 | ||
|
|
e27f36d2c8 | ||
|
|
d582eddc03 | ||
|
|
3b83d8744c | ||
|
|
68a5217b1c | ||
|
|
c01082b822 | ||
|
|
bb95c86320 | ||
|
|
d398926aeb | ||
|
|
7b23efb0ce | ||
|
|
7818ff197c | ||
|
|
95bfc4f8c5 | ||
|
|
c750016034 | ||
|
|
6298e501cd | ||
|
|
7c39b95025 | ||
|
|
02c1e630d1 | ||
|
|
8f3b0cc536 | ||
|
|
d0c5ef8e8a | ||
|
|
d628024637 | ||
|
|
fe1249630b | ||
|
|
cdbe3cf728 | ||
|
|
f5e36bc0cf | ||
|
|
4f9bfe13cb | ||
|
|
959da21b9d | ||
|
|
fae2b1f416 | ||
|
|
a054633d0a | ||
|
|
a4619547f1 | ||
|
|
5d53febb15 | ||
|
|
3da1587924 | ||
|
|
971d0307fe | ||
|
|
705d3afb78 | ||
|
|
bc34b4b9c1 | ||
|
|
a2ac01fcdc | ||
|
|
645ebddd4f | ||
|
|
d5d8a828ef | ||
|
|
96d4193d2d | ||
|
|
18e4afdff9 | ||
|
|
19a702ffc4 | ||
|
|
cfd44c256f | ||
|
|
4cea8d0090 | ||
|
|
30e166b29c | ||
|
|
c50f3d8c5b | ||
|
|
d9249eeedb | ||
|
|
51699cafe6 | ||
|
|
ce97976bdd | ||
|
|
7b8a12e3eb | ||
|
|
7276c37c54 | ||
|
|
4ec7de611c | ||
|
|
0295c40b02 | ||
|
|
035fa44523 | ||
|
|
60c6618db1 | ||
|
|
f35643b2b6 | ||
|
|
b25661c436 | ||
|
|
cea06f0c7e | ||
|
|
7de6a821d9 | ||
|
|
13be4e4eb1 | ||
|
|
691e7c3ba7 | ||
|
|
31aea1cdbf | ||
|
|
97d5642427 | ||
|
|
c68272c1a4 | ||
|
|
a69dc994b3 | ||
|
|
d79a71c764 | ||
|
|
35183de477 | ||
|
|
21b9fe3cb2 | ||
|
|
5762b0bfc3 | ||
|
|
3473e91747 | ||
|
|
6277d7d41d | ||
|
|
f22da2b771 | ||
|
|
3466bbd64a | ||
|
|
49fc87621c | ||
|
|
7e558f4478 | ||
|
|
307624ca9a | ||
|
|
40e66b919a | ||
|
|
cf2e0aadd5 | ||
|
|
1b523f6a78 | ||
|
|
6a9f6bb645 | ||
|
|
45602b05c6 | ||
|
|
8dc842e125 | ||
|
|
9dfc3a49fe | ||
|
|
97d4a61140 | ||
|
|
8b596e2274 | ||
|
|
123b86830b | ||
|
|
e11f5615a1 | ||
|
|
31fb2ed2b1 | ||
|
|
9c12d11c85 | ||
|
|
aea0e885c3 | ||
|
|
f34ff71145 | ||
|
|
820c16e86b | ||
|
|
a335348b1a | ||
|
|
7f134e1995 | ||
|
|
9e13c1c3db | ||
|
|
61161cef0f | ||
|
|
9ff74e789c | ||
|
|
45183df640 | ||
|
|
22f11994f5 | ||
|
|
bc7aa832b7 | ||
|
|
b32f78ea9f | ||
|
|
78002dac3a | ||
|
|
aca70354e7 | ||
|
|
d7a3471b9b | ||
|
|
e81092d42c | ||
|
|
d06df3c819 | ||
|
|
050bfea8f2 | ||
|
|
7b84c74abf | ||
|
|
009c49cd89 | ||
|
|
ca023f73f4 | ||
|
|
456dfd2aac | ||
|
|
0c0291127c | ||
|
|
fe0c74de23 | ||
|
|
df1efa7604 | ||
|
|
73122fc517 | ||
|
|
b506ba277c | ||
|
|
ea3aed8fae | ||
|
|
481bb8e7d4 | ||
|
|
0ed8c40dbf | ||
|
|
d267d7c3b0 | ||
|
|
a04206f600 | ||
|
|
8d64a6f1b2 | ||
|
|
b3f614e150 | ||
|
|
988fae56fd | ||
|
|
fa439fc62d | ||
|
|
c58b2d6156 | ||
|
|
62c56b7918 | ||
|
|
73882ab55d | ||
|
|
490ea2b521 | ||
|
|
7116871652 | ||
|
|
cca374c774 | ||
|
|
3e0dbf92d0 | ||
|
|
2747ae674c | ||
|
|
e85ec5aaba | ||
|
|
d4894e066b | ||
|
|
71a4e84d7c | ||
|
|
d6c36146ba | ||
|
|
9013778f58 | ||
|
|
1af03dd145 | ||
|
|
b83e0fd092 | ||
|
|
1f8d786d05 | ||
|
|
5d87a4ada0 | ||
|
|
4d4fb1e398 | ||
|
|
05eea54f06 | ||
|
|
a00d9ef765 | ||
|
|
aa6ddac9c5 | ||
|
|
9a6f17c745 | ||
|
|
980977aaf3 | ||
|
|
3ed0afaf91 | ||
|
|
250adb4b33 | ||
|
|
0baf09cb53 | ||
|
|
d04a914b9b | ||
|
|
59775cb27c | ||
|
|
fb76cd7b1b | ||
|
|
52fb5306b6 | ||
|
|
74f7d95718 | ||
|
|
bcc1b78c7c | ||
|
|
3978abea87 | ||
|
|
556b726349 | ||
|
|
e0f16f31e6 | ||
|
|
b39e24467c | ||
|
|
65613f07ba | ||
|
|
666a7e4d77 | ||
|
|
cf08bfdd9b | ||
|
|
1f47d56c55 | ||
|
|
1a7dde848f | ||
|
|
c8207308a8 | ||
|
|
f23020cc6f | ||
|
|
555e487f1e | ||
|
|
107bb0b15d | ||
|
|
87a2de393e | ||
|
|
a7e4412454 | ||
|
|
7dce4cfa8e | ||
|
|
2b30aca47c | ||
|
|
0073d2862b | ||
|
|
b182c97185 | ||
|
|
a32119bada | ||
|
|
a928cfd066 | ||
|
|
8c7a371336 | ||
|
|
6f61eb4946 | ||
|
|
1096f05344 | ||
|
|
26f96d3e38 | ||
|
|
4a02189a59 | ||
|
|
7a5bab41f6 | ||
|
|
84c32fb28d |
No files matched your search
@@ -0,0 +1 @@
|
||||
*.sh eol=lf
|
||||
+12
-23
@@ -21,7 +21,7 @@ jobs:
|
||||
java-version: 21
|
||||
|
||||
- name: Install Rust Toolchain
|
||||
run: rustup update && rustup target add armv7-linux-androideabi aarch64-linux-android i686-linux-android x86_64-linux-android && rustup component add rustfmt clippy
|
||||
run: rustup update && rustup target add armv7-linux-androideabi aarch64-linux-android x86_64-linux-android && rustup component add rustfmt clippy
|
||||
|
||||
- name: Rust Cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
@@ -35,16 +35,19 @@ jobs:
|
||||
|
||||
- name: Clippy Check
|
||||
working-directory: ./app/src/main/rust
|
||||
run: cargo clippy --all-targets --all-features -- -D warnings
|
||||
run: cargo clippy --target aarch64-linux-android --all-features -- -D warnings
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v3
|
||||
uses: gradle/actions/setup-gradle@v4
|
||||
with:
|
||||
cache-read-only: true
|
||||
|
||||
- name: Spotless Check
|
||||
run: ./gradlew spotlessCheck
|
||||
|
||||
- name: Lint Check
|
||||
run: ./gradlew lintMarshmallowRelease
|
||||
|
||||
default:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@@ -58,7 +61,7 @@ jobs:
|
||||
java-version: 21
|
||||
|
||||
- name: Install Rust Toolchain
|
||||
run: rustup update && rustup target add armv7-linux-androideabi aarch64-linux-android i686-linux-android x86_64-linux-android && rustup component add rustfmt clippy
|
||||
run: rustup update && rustup target add armv7-linux-androideabi aarch64-linux-android x86_64-linux-android && rustup component add rustfmt clippy
|
||||
|
||||
- name: Rust Cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
@@ -68,10 +71,9 @@ jobs:
|
||||
save-if: "false"
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v3
|
||||
uses: gradle/actions/setup-gradle@v4
|
||||
with:
|
||||
cache-encryption-key: ${{ secrets.GradleEncryptionKey }}
|
||||
gradle-home-cache-cleanup: true
|
||||
|
||||
- name: CMake Cache
|
||||
uses: actions/cache@v4
|
||||
@@ -101,17 +103,11 @@ jobs:
|
||||
name: default-armeabi-v7a-${{ github.sha }}
|
||||
path: app/build/outputs/apk/default/release/app-default-armeabi-v7a-release.apk
|
||||
|
||||
- name: Upload x86
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: default-x86-${{ github.sha }}
|
||||
path: app/build/outputs/apk/default/release/app-default-x86-release.apk
|
||||
|
||||
- name: Upload mapping
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: default-mapping-${{ github.sha }}
|
||||
path: app/build/outputs/mapping/defaultRelease/mapping.txt
|
||||
path: app/build/outputs/mapping/defaultRelease
|
||||
|
||||
- name: Upload native debug symbols
|
||||
uses: actions/upload-artifact@v4
|
||||
@@ -132,7 +128,7 @@ jobs:
|
||||
java-version: 21
|
||||
|
||||
- name: Install Rust Toolchain
|
||||
run: rustup update && rustup target add armv7-linux-androideabi aarch64-linux-android i686-linux-android x86_64-linux-android && rustup component add rustfmt clippy
|
||||
run: rustup update && rustup target add armv7-linux-androideabi aarch64-linux-android x86_64-linux-android && rustup component add rustfmt clippy
|
||||
|
||||
- name: Rust Cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
@@ -142,10 +138,9 @@ jobs:
|
||||
save-if: "false"
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v3
|
||||
uses: gradle/actions/setup-gradle@v4
|
||||
with:
|
||||
cache-encryption-key: ${{ secrets.GradleEncryptionKey }}
|
||||
gradle-home-cache-cleanup: true
|
||||
|
||||
- name: CMake Cache
|
||||
uses: actions/cache@v4
|
||||
@@ -175,17 +170,11 @@ jobs:
|
||||
name: marshmallow-armeabi-v7a-${{ github.sha }}
|
||||
path: app/build/outputs/apk/marshmallow/release/app-marshmallow-armeabi-v7a-release.apk
|
||||
|
||||
- name: Upload x86
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: marshmallow-x86-${{ github.sha }}
|
||||
path: app/build/outputs/apk/marshmallow/release/app-marshmallow-x86-release.apk
|
||||
|
||||
- name: Upload mapping
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: marshmallow-mapping-${{ github.sha }}
|
||||
path: app/build/outputs/mapping/marshmallowRelease/mapping.txt
|
||||
path: app/build/outputs/mapping/marshmallowRelease
|
||||
|
||||
- name: Upload native debug symbols
|
||||
uses: actions/upload-artifact@v4
|
||||
|
||||
@@ -2,7 +2,7 @@ name: 'Lock Threads'
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
- cron: '0 8 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
|
||||
@@ -18,8 +18,19 @@ jobs:
|
||||
distribution: temurin
|
||||
java-version: 21
|
||||
|
||||
- name: Free disk space
|
||||
run: |
|
||||
shopt -s extglob
|
||||
rm -rf ${ANDROID_SDK_ROOT}/ndk/!(27*)
|
||||
|
||||
- name: Enable KVM group perms
|
||||
run: |
|
||||
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
|
||||
sudo udevadm control --reload-rules
|
||||
sudo udevadm trigger --name-match=kvm
|
||||
|
||||
- name: Install Rust Toolchain
|
||||
run: rustup update && rustup target add armv7-linux-androideabi aarch64-linux-android i686-linux-android x86_64-linux-android
|
||||
run: rustup update && rustup target add armv7-linux-androideabi aarch64-linux-android x86_64-linux-android
|
||||
|
||||
- name: Rust Cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
@@ -33,17 +44,23 @@ jobs:
|
||||
key: cmake-${{ hashFiles('gradle/libs.versions.toml', 'app/build.gradle.kts', 'app/src/main/cpp/CMakeLists.txt') }}
|
||||
restore-keys: cmake-
|
||||
|
||||
- name: Gradle Build
|
||||
uses: gradle/actions/setup-gradle@v3
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v4
|
||||
with:
|
||||
arguments: assembleRelease
|
||||
cache-encryption-key: ${{ secrets.GradleEncryptionKey }}
|
||||
|
||||
- name: Generate Baseline Profile
|
||||
run: ./gradlew generateBaselineProfile
|
||||
-Pandroid.testoptions.manageddevices.emulator.gpu="swiftshader_indirect"
|
||||
|
||||
- name: Build
|
||||
run: ./gradlew assembleRelease
|
||||
|
||||
- name: Rename Apks
|
||||
run: |
|
||||
mv app/build/outputs/apk/default/release/app-default-universal-release.apk EhViewer-${{ github.ref_name }}-default-universal.apk
|
||||
mv app/build/outputs/apk/default/release/app-default-arm64-v8a-release.apk EhViewer-${{ github.ref_name }}-default-arm64-v8a.apk
|
||||
mv app/build/outputs/apk/default/release/app-default-x86_64-release.apk EhViewer-${{ github.ref_name }}-default-x86_64.apk
|
||||
mv app/build/outputs/apk/default/release/app-default-x86-release.apk EhViewer-${{ github.ref_name }}-default-x86.apk
|
||||
mv app/build/outputs/apk/default/release/app-default-armeabi-v7a-release.apk EhViewer-${{ github.ref_name }}-default-armeabi-v7a.apk
|
||||
mv app/build/outputs/mapping/defaultRelease/mapping.txt EhViewer-${{ github.ref_name }}-default-mapping.txt
|
||||
mv app/build/outputs/native-debug-symbols/defaultRelease/native-debug-symbols.zip EhViewer-${{ github.ref_name }}-default-native-debug-symbols.zip
|
||||
@@ -51,7 +68,6 @@ jobs:
|
||||
mv app/build/outputs/apk/marshmallow/release/app-marshmallow-universal-release.apk EhViewer-${{ github.ref_name }}-marshmallow-universal.apk
|
||||
mv app/build/outputs/apk/marshmallow/release/app-marshmallow-arm64-v8a-release.apk EhViewer-${{ github.ref_name }}-marshmallow-arm64-v8a.apk
|
||||
mv app/build/outputs/apk/marshmallow/release/app-marshmallow-x86_64-release.apk EhViewer-${{ github.ref_name }}-marshmallow-x86_64.apk
|
||||
mv app/build/outputs/apk/marshmallow/release/app-marshmallow-x86-release.apk EhViewer-${{ github.ref_name }}-marshmallow-x86.apk
|
||||
mv app/build/outputs/apk/marshmallow/release/app-marshmallow-armeabi-v7a-release.apk EhViewer-${{ github.ref_name }}-marshmallow-armeabi-v7a.apk
|
||||
mv app/build/outputs/mapping/marshmallowRelease/mapping.txt EhViewer-${{ github.ref_name }}-marshmallow-mapping.txt
|
||||
mv app/build/outputs/native-debug-symbols/marshmallowRelease/native-debug-symbols.zip EhViewer-${{ github.ref_name }}-marshmallow-native-debug-symbols.zip
|
||||
@@ -64,7 +80,6 @@ jobs:
|
||||
EhViewer-${{ github.ref_name }}-default-universal.apk
|
||||
EhViewer-${{ github.ref_name }}-default-arm64-v8a.apk
|
||||
EhViewer-${{ github.ref_name }}-default-x86_64.apk
|
||||
EhViewer-${{ github.ref_name }}-default-x86.apk
|
||||
EhViewer-${{ github.ref_name }}-default-armeabi-v7a.apk
|
||||
EhViewer-${{ github.ref_name }}-default-mapping.txt
|
||||
EhViewer-${{ github.ref_name }}-default-native-debug-symbols.zip
|
||||
@@ -72,7 +87,6 @@ jobs:
|
||||
EhViewer-${{ github.ref_name }}-marshmallow-universal.apk
|
||||
EhViewer-${{ github.ref_name }}-marshmallow-arm64-v8a.apk
|
||||
EhViewer-${{ github.ref_name }}-marshmallow-x86_64.apk
|
||||
EhViewer-${{ github.ref_name }}-marshmallow-x86.apk
|
||||
EhViewer-${{ github.ref_name }}-marshmallow-armeabi-v7a.apk
|
||||
EhViewer-${{ github.ref_name }}-marshmallow-mapping.txt
|
||||
EhViewer-${{ github.ref_name }}-marshmallow-native-debug-symbols.zip
|
||||
@@ -0,0 +1,29 @@
|
||||
name: 'Update Chrome Version'
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 1 * *'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
action:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Update Chrome Version
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
cd chrome-for-testing
|
||||
curl -OLf https://googlechromelabs.github.io/chrome-for-testing/LATEST_RELEASE_STABLE
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git config push.autoSetupRemote true
|
||||
git switch -c chore/chrome
|
||||
if git commit -am "Update Chrome version for default User-Agent"; then
|
||||
git push
|
||||
gh pr create -f
|
||||
gh pr merge chore/chrome --auto -dr
|
||||
fi
|
||||
@@ -1,5 +1,183 @@
|
||||
# 变更日志
|
||||
|
||||
## [1.12.1] - 2024-09-28
|
||||
|
||||
### 新功能
|
||||
|
||||
* 屏蔽含有二维码的图片(位于高级设置)
|
||||
|
||||
### 改进
|
||||
|
||||
* 移除高级设置中的自定义 User Agent,改为桌面版网站选项
|
||||
* 画廊预览与画廊详情页面合并
|
||||
* 为快速搜索和下载标签列表添加了滚动条
|
||||
* 无网络条件下启动时自动转到下载页面
|
||||
* 杂项优化
|
||||
|
||||
### Bug 修复
|
||||
|
||||
* 从左到右或右到左模式观看使用鼠标滚轮无法翻页 [#1541](https://github.com/FooIbar/EhViewer/issues/1541)
|
||||
* 状态栏颜色未跟随阅读器主题变化 [#1542](https://github.com/FooIbar/EhViewer/issues/1542)
|
||||
* 打开 EH 设置时闪退 [#1563](https://github.com/FooIbar/EhViewer/issues/1563)
|
||||
* 画廊种子过多时显示没有种子 [#1561](https://github.com/FooIbar/EhViewer/issues/1561)
|
||||
* 加载大于 100 MB 的位图时崩溃
|
||||
* 阅读器加载小圆圈在黑色背景下不自动变色 [#1579](https://github.com/FooIbar/EhViewer/issues/1579)
|
||||
* 因闪退造成的无法挽回的下载失败 [#1602](https://github.com/FooIbar/EhViewer/issues/1602)
|
||||
* 某些情况下闪退 [#1548](https://github.com/FooIbar/EhViewer/issues/1548) [#1555](https://github.com/FooIbar/EhViewer/issues/1555) [#1568](https://github.com/FooIbar/EhViewer/issues/1568)
|
||||
* Android 7.0 及以下的设备无法加载图片 [#1633](https://github.com/FooIbar/EhViewer/issues/1633)
|
||||
* 某些情况下导入数据失败 [#1634](https://github.com/FooIbar/EhViewer/issues/1634)
|
||||
* 下载时创建重复文件夹 [#1619](https://github.com/FooIbar/EhViewer/issues/1619)
|
||||
* 滚动方向改变后预载图片不生效 [#1560](https://github.com/FooIbar/EhViewer/issues/1560)
|
||||
* 选择无效的下载位置后闪退
|
||||
* 杂项修复
|
||||
|
||||
|
||||
## [1.12.0] - 2024-08-19
|
||||
|
||||
### 重大变更
|
||||
|
||||
* Compose 阅读器已稳定,移除了旧阅读器实现 [#847](https://github.com/FooIbar/EhViewer/issues/847)
|
||||
* 由于 E 站缩略图服务器变更,移除了强制使用 e-hentai 缩略图服务器的选项
|
||||
|
||||
### 改进
|
||||
|
||||
* 更新高级搜索选项中页数过滤器的范围限制以遵循 E 站变更
|
||||
|
||||
### Bug 修复
|
||||
|
||||
* 评论页面刷新指示器边距不正确 [#1428](https://github.com/FooIbar/EhViewer/issues/1428)
|
||||
* 关闭打开失败的压缩包时崩溃
|
||||
* 无法打开带密码的压缩包 [#1454](https://github.com/FooIbar/EhViewer/issues/1454)
|
||||
* 使用音量键翻页时无法关闭过渡动画 [#1467](https://github.com/FooIbar/EhViewer/issues/1467)
|
||||
* 带有图片的评论显示错位 [#1468](https://github.com/FooIbar/EhViewer/issues/1468)
|
||||
* 在包含相同项目的页面间切换时崩溃 [#1490](https://github.com/FooIbar/EhViewer/issues/1490)
|
||||
* 无法检查更新 [#1512](https://github.com/FooIbar/EhViewer/issues/1512)
|
||||
* 画廊详情未加载完成时退出阅读器导致崩溃 [#1507](https://github.com/FooIbar/EhViewer/issues/1507)
|
||||
* 无法为带有临时标签的画廊生成 ComicInfo.xml [#1509](https://github.com/FooIbar/EhViewer/issues/1509)
|
||||
* 已下载的画廊翻页时出现加载动画 [#1510](https://github.com/FooIbar/EhViewer/issues/1510)
|
||||
* 画廊详情页面的缩略图无法加载 [#1529](https://github.com/FooIbar/EhViewer/issues/1529)
|
||||
* 图片未加载完成时无法通过点击翻页
|
||||
* 搜索结果为空时解析失败 [#1418](https://github.com/FooIbar/EhViewer/issues/1418)
|
||||
* 缩略图分辨率设置不起作用
|
||||
|
||||
|
||||
## [1.11.7] - 2024-07-10
|
||||
|
||||
### Bug 修复
|
||||
|
||||
* 无法下载标题包含 # 或 % 的画廊
|
||||
* 下载页数较多的画廊时应用无响应
|
||||
* 退出搜索栏时文本闪烁
|
||||
|
||||
|
||||
## [1.11.6] - 2024-07-09 [YANKED]
|
||||
|
||||
### 改进
|
||||
|
||||
* 某些设备上无法加载长图时可尝试减小 设置-高级 中的硬件位图阈值 [#1321](https://github.com/FooIbar/EhViewer/issues/1321)
|
||||
* 清理下载冗余时显示确认对话框 [#1369](https://github.com/FooIbar/EhViewer/issues/1369)
|
||||
* 修改收藏时显示收藏备注 [#1392](https://github.com/FooIbar/EhViewer/issues/1392)
|
||||
|
||||
### Bug 修复
|
||||
|
||||
* 图片完整性校验误报 [#1286](https://github.com/FooIbar/EhViewer/issues/1286)
|
||||
* 某些情况下应用崩溃 [#1290](https://github.com/FooIbar/EhViewer/issues/1290) [#1302](https://github.com/FooIbar/EhViewer/issues/1302)
|
||||
* 某些情况下渲染问题 [#1284](https://github.com/FooIbar/EhViewer/issues/1284)
|
||||
* 无法下载标题过长的画廊 [#1340](https://github.com/FooIbar/EhViewer/issues/1340)
|
||||
* 某些界面拖动滚动条时黑屏 [#1329](https://github.com/FooIbar/EhViewer/issues/1329)
|
||||
* 搜索栏多行文本遮挡其他内容 [#1365](https://github.com/FooIbar/EhViewer/issues/1365)
|
||||
* CIFS Documents Provider 兼容问题 [#1356](https://github.com/FooIbar/EhViewer/issues/1356)
|
||||
* 清理下载冗余未按预期运行 [#1369](https://github.com/FooIbar/EhViewer/issues/1369)
|
||||
* 读取压缩包失败时崩溃
|
||||
* 某些设备上崩溃 [#1276](https://github.com/FooIbar/EhViewer/issues/1276)
|
||||
* 检查更新失效
|
||||
|
||||
|
||||
## [1.11.6-RC2] - 2024-06-06
|
||||
|
||||
### 改进
|
||||
|
||||
* 下载图片时执行完整性校验以防止文件损坏/被篡改
|
||||
|
||||
|
||||
## [1.11.6-RC1] - 2024-06-02
|
||||
|
||||
### 改进
|
||||
|
||||
* 下载列表默认显示全部下载项目
|
||||
* 替换搜索文本中的换行符为空格 [#1258](https://github.com/FooIbar/EhViewer/issues/1258)
|
||||
|
||||
### Bug 修复
|
||||
|
||||
* 评价画廊时未使用输入的评分
|
||||
* 搜索时意外选择 Non-H 类别 [#1246](https://github.com/FooIbar/EhViewer/issues/1246)
|
||||
* 侧边栏与导航抽屉重叠 [#1262](https://github.com/FooIbar/EhViewer/issues/1262)
|
||||
* 快速搜索/下载标签排序失效 [#1267](https://github.com/FooIbar/EhViewer/issues/1267)
|
||||
* 加载压缩包/种子列表出错时 UI 冻结
|
||||
* 低版本 Android 上某些 UI 被遮挡
|
||||
|
||||
|
||||
## [1.11.5] - 2024-05-19
|
||||
|
||||
### 重大变更
|
||||
|
||||
* 不再支持 32 位 x86
|
||||
|
||||
### 新功能
|
||||
|
||||
* 支持按作者对下载分组
|
||||
* 由于 E 站不再强制要求人机验证,重新支持账号密码登录
|
||||
|
||||
### 改进
|
||||
|
||||
* 在登录时加载收藏名称 [#1077](https://github.com/FooIbar/EhViewer/issues/1077)
|
||||
* 优化归档和种子界面 UI
|
||||
* 禁止在 cookie 界面截图以防止泄露
|
||||
* 在缩略图模式下显示画廊页数
|
||||
* 记住最低评分和页数搜索参数
|
||||
* 更新 User-Agent 以缓解 IP 封禁 [#1182](https://github.com/FooIbar/EhViewer/issues/1182)
|
||||
* 账号密码登录支持自动填充
|
||||
* 更新中文(台灣)翻译
|
||||
|
||||
### Bug 修复
|
||||
|
||||
* 非触摸模式下无法退出搜索界面 [#1060](https://github.com/FooIbar/EhViewer/issues/1060)
|
||||
* 某些设备上无法安装更新 [#1067](https://github.com/FooIbar/EhViewer/issues/1067)
|
||||
* 某些设备上崩溃/卡顿 [#996](https://github.com/FooIbar/EhViewer/issues/996) [#1023](https://github.com/FooIbar/EhViewer/issues/1023)
|
||||
* 搜索记录中存在重复条目时崩溃 [#1130](https://github.com/FooIbar/EhViewer/issues/1130)
|
||||
* 条漫模式下缩小后点按区域偏移 [#127](https://github.com/FooIbar/EhViewer/issues/127)
|
||||
* 多窗口模式下点按区域偏移
|
||||
* 点击下载通知时未清除状态
|
||||
* 保存未完成加载的图片时崩溃 [#1154](https://github.com/FooIbar/EhViewer/issues/1154)
|
||||
* 历史记录界面无法在项目上滑动打开抽屉 [#464](https://github.com/FooIbar/EhViewer/issues/464)
|
||||
* 导入数据失败时显示导入成功 [#1174](https://github.com/FooIbar/EhViewer/issues/1174)
|
||||
* 某些条件下收藏界面崩溃 [#1190](https://github.com/FooIbar/EhViewer/issues/1190)
|
||||
* 反转点按区域功能失效 [#1217](https://github.com/FooIbar/EhViewer/issues/1217)
|
||||
|
||||
### 已知问题
|
||||
|
||||
* 某些设备上从后台返回时某些 UI 消失,可通过关闭列表项目动画部分缓解 [#1184](https://github.com/FooIbar/EhViewer/issues/1184)
|
||||
|
||||
|
||||
## [1.11.4] - 2024-04-21
|
||||
|
||||
### 新功能
|
||||
|
||||
* 更新前自动备份数据
|
||||
|
||||
### 改进
|
||||
|
||||
* 在 设置-高级 中加入了禁用列表项目动画的选项,该功能在部分设备上导致崩溃/卡顿 [#996](https://github.com/FooIbar/EhViewer/issues/996) [#1023](https://github.com/FooIbar/EhViewer/issues/1023)
|
||||
* 下载列表多选模式下避免误触 [#1016](https://github.com/FooIbar/EhViewer/issues/1016)
|
||||
* 杂项 UI 优化
|
||||
|
||||
### Bug 修复
|
||||
|
||||
* 重命名下载标签后删除该标签时崩溃 [#1008](https://github.com/FooIbar/EhViewer/issues/1008)
|
||||
* 搜索栏中某些输入法长按退格无法连续删除 [#606](https://github.com/FooIbar/EhViewer/issues/606)
|
||||
* 评论/屏蔽/搜索建议/下载标签/快速搜索列表卡顿 [#1041](https://github.com/FooIbar/EhViewer/issues/1041)
|
||||
|
||||
|
||||
## [1.11.3] - 2024-04-06
|
||||
|
||||
### 改进
|
||||
|
||||
@@ -85,17 +85,15 @@ and [Dynamic Color](https://m3.material.io/styles/color/dynamic-color/overview)
|
||||
Here is the libraries
|
||||
|
||||
- [Arrow](https://arrow-kt.io/)
|
||||
- [AOSP & AndroidX](http://source.android.com/)
|
||||
- [AOSP & AndroidX](https://source.android.com/)
|
||||
- [Kotlin & KotlinX](https://kotlinlang.org/)
|
||||
- [Material Icons](https://github.com/google/material-design-icons)
|
||||
- [Ktor](https://ktor.io/)
|
||||
- [Coil](https://coil-kt.github.io/coil/)
|
||||
- [Compose Destinations](https://composedestinations.rafaelcosta.xyz/)
|
||||
- [libarchive](http://www.libarchive.org/)
|
||||
- [libarchive](https://www.libarchive.org/)
|
||||
- [jsoup](https://jsoup.org/)
|
||||
|
||||
Specially thanks [Tachiyomi](https://tachiyomi.org/) for its reader implementation!
|
||||
|
||||
# License
|
||||
|
||||
Copyright 2014-2019 Hippo Seven
|
||||
|
||||
+101
-77
@@ -1,8 +1,6 @@
|
||||
import com.mikepenz.aboutlibraries.plugin.DuplicateMode.MERGE
|
||||
import com.mikepenz.aboutlibraries.plugin.DuplicateRule.GROUP
|
||||
import java.time.Instant
|
||||
import java.time.ZoneOffset
|
||||
import java.time.format.DateTimeFormatter
|
||||
import org.jetbrains.kotlin.compose.compiler.gradle.ComposeFeatureFlag
|
||||
|
||||
val isRelease: Boolean
|
||||
get() = gradle.startParameter.taskNames.any { it.contains("Release") }
|
||||
@@ -10,24 +8,47 @@ val isRelease: Boolean
|
||||
plugins {
|
||||
alias(libs.plugins.android.application)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
alias(libs.plugins.kotlin.compose)
|
||||
alias(libs.plugins.kotlin.parcelize)
|
||||
alias(libs.plugins.kotlin.serialization)
|
||||
alias(libs.plugins.ksp)
|
||||
alias(libs.plugins.spotless)
|
||||
alias(libs.plugins.aboutlibrariesPlugin)
|
||||
alias(libs.plugins.composeCompilerReportGenerator)
|
||||
alias(libs.plugins.baselineprofile)
|
||||
}
|
||||
|
||||
val supportedAbis = arrayOf("arm64-v8a", "x86_64", "armeabi-v7a")
|
||||
|
||||
android {
|
||||
compileSdk = 34
|
||||
ndkVersion = "26.2.11394342"
|
||||
compileSdk = 35
|
||||
buildToolsVersion = "35.0.0"
|
||||
ndkVersion = "27.1.12297006"
|
||||
|
||||
androidResources {
|
||||
generateLocaleConfig = true
|
||||
localeFilters += listOf(
|
||||
"zh",
|
||||
"zh-rCN",
|
||||
"zh-rHK",
|
||||
"zh-rTW",
|
||||
"es",
|
||||
"ja",
|
||||
"ko",
|
||||
"fr",
|
||||
"de",
|
||||
"th",
|
||||
"tr",
|
||||
"nb-rNO",
|
||||
)
|
||||
}
|
||||
|
||||
splits {
|
||||
abi {
|
||||
isEnable = true
|
||||
reset()
|
||||
if (isRelease) {
|
||||
include("arm64-v8a", "x86_64", "armeabi-v7a", "x86")
|
||||
include(*supportedAbis)
|
||||
isUniversalApk = true
|
||||
} else {
|
||||
include("arm64-v8a", "x86_64")
|
||||
@@ -48,44 +69,41 @@ android {
|
||||
commandLine = "git rev-parse --short=7 HEAD".split(' ')
|
||||
}.standardOutput.asText.get().trim()
|
||||
|
||||
val buildTime by lazy {
|
||||
val formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm").withZone(ZoneOffset.UTC)
|
||||
formatter.format(Instant.now())
|
||||
}
|
||||
val commitTime = providers.exec {
|
||||
commandLine = "git log -1 --format=%ct".split(' ')
|
||||
}.standardOutput.asText.get().trim()
|
||||
|
||||
val repoName = providers.exec {
|
||||
commandLine = "git remote get-url origin".split(' ')
|
||||
}.standardOutput.asText.get().trim().removePrefix("https://github.com/").removePrefix("git@github.com:")
|
||||
.removeSuffix(".git")
|
||||
|
||||
val chromeVersion = rootProject.layout.projectDirectory.file("chrome-for-testing/LATEST_RELEASE_STABLE").asFile
|
||||
.readText().substringBefore('.')
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "moe.tarsin.ehviewer"
|
||||
minSdk = 26
|
||||
targetSdk = 34
|
||||
versionCode = 180054
|
||||
versionName = "1.11.3"
|
||||
resourceConfigurations.addAll(
|
||||
listOf(
|
||||
"zh",
|
||||
"zh-rCN",
|
||||
"zh-rHK",
|
||||
"zh-rTW",
|
||||
"es",
|
||||
"ja",
|
||||
"ko",
|
||||
"fr",
|
||||
"de",
|
||||
"th",
|
||||
"tr",
|
||||
"nb-rNO",
|
||||
),
|
||||
)
|
||||
targetSdk = 35
|
||||
versionCode = 180060
|
||||
versionName = "1.12.1"
|
||||
// versionNameSuffix = "-SNAPSHOT"
|
||||
buildConfigField("String", "RAW_VERSION_NAME", "\"$versionName${versionNameSuffix.orEmpty()}\"")
|
||||
buildConfigField("String", "COMMIT_SHA", "\"$commitSha\"")
|
||||
buildConfigField("long", "COMMIT_TIME", commitTime)
|
||||
buildConfigField("String", "REPO_NAME", "\"$repoName\"")
|
||||
buildConfigField("String", "CHROME_VERSION", "\"$chromeVersion\"")
|
||||
ndk {
|
||||
if (isRelease) {
|
||||
abiFilters.addAll(supportedAbis)
|
||||
}
|
||||
debugSymbolLevel = "FULL"
|
||||
}
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
arguments += "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
flavorDimensions += "api"
|
||||
@@ -109,37 +127,10 @@ android {
|
||||
isCoreLibraryDesugaringEnabled = true
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
freeCompilerArgs = listOf(
|
||||
// https://kotlinlang.org/docs/compiler-reference.html#progressive
|
||||
"-progressive",
|
||||
"-Xjvm-default=all",
|
||||
"-Xlambdas=indy",
|
||||
"-Xcontext-receivers",
|
||||
|
||||
"-opt-in=coil3.annotation.ExperimentalCoilApi",
|
||||
"-opt-in=androidx.compose.foundation.layout.ExperimentalLayoutApi",
|
||||
"-opt-in=androidx.compose.material3.ExperimentalMaterial3Api",
|
||||
"-opt-in=androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi",
|
||||
"-opt-in=androidx.compose.ui.ExperimentalComposeUiApi",
|
||||
"-opt-in=androidx.compose.foundation.ExperimentalFoundationApi",
|
||||
"-opt-in=androidx.compose.animation.ExperimentalAnimationApi",
|
||||
"-opt-in=androidx.paging.ExperimentalPagingApi",
|
||||
"-opt-in=kotlin.contracts.ExperimentalContracts",
|
||||
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
||||
"-opt-in=kotlinx.coroutines.FlowPreview",
|
||||
"-opt-in=kotlinx.serialization.ExperimentalSerializationApi",
|
||||
"-opt-in=splitties.experimental.ExperimentalSplittiesApi",
|
||||
"-opt-in=splitties.preferences.DataStorePreferencesPreview",
|
||||
|
||||
"-P", "plugin:androidx.compose.compiler.plugins.kotlin:experimentalStrongSkipping=true",
|
||||
"-P", "plugin:androidx.compose.compiler.plugins.kotlin:nonSkippingGroupOptimization=true",
|
||||
)
|
||||
}
|
||||
|
||||
lint {
|
||||
checkReleaseBuilds = false
|
||||
disable += setOf("MissingTranslation", "MissingQuantity")
|
||||
fatal += setOf("NewApi", "InlinedApi")
|
||||
error += setOf("InlinedApi")
|
||||
}
|
||||
|
||||
packaging {
|
||||
@@ -156,27 +147,31 @@ android {
|
||||
isShrinkResources = true
|
||||
proguardFiles("proguard-rules.pro")
|
||||
signingConfig = signConfig
|
||||
buildConfigField("String", "BUILD_TIME", "\"$buildTime\"")
|
||||
}
|
||||
debug {
|
||||
applicationIdSuffix = ".debug"
|
||||
buildConfigField("String", "BUILD_TIME", "\"\"")
|
||||
}
|
||||
create("benchmarkRelease") {
|
||||
initWith(buildTypes.getByName("release"))
|
||||
matchingFallbacks += listOf("release")
|
||||
applicationIdSuffix = ".benchmark"
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
isDebuggable = false
|
||||
}
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
buildConfig = true
|
||||
compose = true
|
||||
viewBinding = true
|
||||
}
|
||||
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion = libs.versions.compose.compiler.get()
|
||||
}
|
||||
|
||||
namespace = "com.hippo.ehviewer"
|
||||
}
|
||||
|
||||
composeCompiler {
|
||||
featureFlags = setOf(ComposeFeatureFlag.OptimizeNonSkippingGroups)
|
||||
}
|
||||
|
||||
androidComponents {
|
||||
onVariants(selector().withBuildType("release")) {
|
||||
it.packaging.resources.excludes.addAll(
|
||||
@@ -188,6 +183,10 @@ androidComponents {
|
||||
}
|
||||
}
|
||||
|
||||
baselineProfile {
|
||||
mergeIntoMain = true
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// https://developer.android.com/jetpack/androidx/releases/activity
|
||||
implementation(libs.androidx.activity.compose)
|
||||
@@ -206,25 +205,21 @@ dependencies {
|
||||
implementation(libs.androidx.core.splashscreen)
|
||||
|
||||
implementation(libs.androidx.constraintlayout.compose)
|
||||
implementation(libs.androidx.datastore)
|
||||
implementation(libs.androidx.graphics.path)
|
||||
|
||||
// https://developer.android.com/jetpack/androidx/releases/lifecycle
|
||||
implementation(libs.androidx.lifecycle.process)
|
||||
implementation(libs.androidx.lifecycle.compose)
|
||||
|
||||
// https://developer.android.com/jetpack/androidx/releases/navigation
|
||||
implementation(libs.androidx.navigation.compose)
|
||||
|
||||
// https://developer.android.com/jetpack/androidx/releases/paging
|
||||
implementation(libs.androidx.paging.compose)
|
||||
|
||||
implementation(libs.androidx.recyclerview)
|
||||
|
||||
// https://developer.android.com/jetpack/androidx/releases/room
|
||||
ksp(libs.androidx.room.compiler)
|
||||
implementation(libs.androidx.room.paging)
|
||||
|
||||
implementation(libs.androidx.work.runtime)
|
||||
implementation(libs.photoview) // Dead Dependency
|
||||
implementation(libs.directionalviewpager) // Dead Dependency
|
||||
implementation(libs.material.motion.core)
|
||||
|
||||
implementation(libs.bundles.splitties)
|
||||
@@ -239,12 +234,11 @@ dependencies {
|
||||
implementation(libs.aboutlibraries.compose.m3)
|
||||
implementation(libs.accompanist.drawable.painter)
|
||||
|
||||
implementation(libs.insetter) // Dead Dependency
|
||||
|
||||
implementation(libs.reorderable)
|
||||
|
||||
implementation(platform(libs.arrow.stack))
|
||||
implementation(libs.arrow.fx.coroutines)
|
||||
implementation(libs.arrow.resilience)
|
||||
|
||||
// https://coil-kt.github.io/coil/changelog/
|
||||
implementation(platform(libs.coil.bom))
|
||||
@@ -264,17 +258,45 @@ dependencies {
|
||||
|
||||
implementation(libs.cronet.embedded)
|
||||
|
||||
implementation(libs.androidx.profileinstaller)
|
||||
"baselineProfile"(project(":benchmark"))
|
||||
|
||||
debugImplementation(libs.compose.ui.tooling)
|
||||
implementation(libs.compose.ui.tooling.preview)
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvmToolchain(21)
|
||||
compilerOptions {
|
||||
freeCompilerArgs = listOf(
|
||||
// https://kotlinlang.org/docs/compiler-reference.html#progressive
|
||||
"-progressive",
|
||||
"-Xjvm-default=all",
|
||||
"-Xcontext-receivers",
|
||||
"-Xwhen-guards",
|
||||
"-Xsuppress-warning=CONTEXT_RECEIVERS_DEPRECATED",
|
||||
|
||||
"-opt-in=coil3.annotation.ExperimentalCoilApi",
|
||||
"-opt-in=androidx.compose.foundation.layout.ExperimentalLayoutApi",
|
||||
"-opt-in=androidx.compose.material3.ExperimentalMaterial3Api",
|
||||
"-opt-in=androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi",
|
||||
"-opt-in=androidx.compose.ui.ExperimentalComposeUiApi",
|
||||
"-opt-in=androidx.compose.foundation.ExperimentalFoundationApi",
|
||||
"-opt-in=androidx.compose.animation.ExperimentalAnimationApi",
|
||||
"-opt-in=androidx.compose.animation.ExperimentalSharedTransitionApi",
|
||||
"-opt-in=androidx.paging.ExperimentalPagingApi",
|
||||
"-opt-in=kotlin.contracts.ExperimentalContracts",
|
||||
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
||||
"-opt-in=kotlinx.coroutines.FlowPreview",
|
||||
"-opt-in=kotlinx.serialization.ExperimentalSerializationApi",
|
||||
"-opt-in=splitties.experimental.ExperimentalSplittiesApi",
|
||||
"-opt-in=splitties.preferences.DataStorePreferencesPreview",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
ksp {
|
||||
arg("room.schemaLocation", "$projectDir/schemas")
|
||||
arg("room.generateKotlin", "true")
|
||||
arg("compose-destinations.codeGenPackageName", "com.hippo.ehviewer.ui")
|
||||
}
|
||||
|
||||
@@ -283,13 +305,15 @@ aboutLibraries {
|
||||
duplicationRule = GROUP
|
||||
}
|
||||
|
||||
val ktlintVersion = libs.ktlint.get().version
|
||||
|
||||
spotless {
|
||||
kotlin {
|
||||
// https://github.com/diffplug/spotless/issues/111
|
||||
target("src/**/*.kt")
|
||||
ktlint()
|
||||
ktlint(ktlintVersion)
|
||||
}
|
||||
kotlinGradle {
|
||||
ktlint()
|
||||
ktlint(ktlintVersion)
|
||||
}
|
||||
}
|
||||
Vendored
+4
-5
@@ -11,16 +11,15 @@
|
||||
public static final ** CREATOR;
|
||||
}
|
||||
|
||||
# Workaround for https://issuetracker.google.com/331556916
|
||||
-keepclassmembers class androidx.biometric.BiometricManager {
|
||||
public int canAuthenticate(int);
|
||||
}
|
||||
|
||||
# Ktor logger
|
||||
-dontwarn org.slf4j.impl.StaticLoggerBinder
|
||||
|
||||
-dontwarn org.conscrypt.Conscrypt
|
||||
|
||||
# https://issuetracker.google.com/222232895
|
||||
-dontwarn androidx.window.extensions.**
|
||||
-dontwarn androidx.window.sidecar.Sidecar*
|
||||
|
||||
-keepattributes LineNumberTable
|
||||
|
||||
-allowaccessmodification
|
||||
|
||||
@@ -0,0 +1,564 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 23,
|
||||
"identityHash": "7d1cfc4e9a9d506a73eb0520ae414d25",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "GALLERIES",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`GID` INTEGER NOT NULL, `TOKEN` TEXT NOT NULL, `TITLE` TEXT, `TITLE_JPN` TEXT, `THUMB` TEXT, `CATEGORY` INTEGER NOT NULL, `POSTED` TEXT, `UPLOADER` TEXT, `RATING` REAL NOT NULL, `SIMPLE_TAGS` TEXT, `PAGES` INTEGER NOT NULL DEFAULT 0, `SIMPLE_LANGUAGE` TEXT, `FAVORITE_SLOT` INTEGER NOT NULL, PRIMARY KEY(`GID`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "gid",
|
||||
"columnName": "GID",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "token",
|
||||
"columnName": "TOKEN",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "title",
|
||||
"columnName": "TITLE",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "titleJpn",
|
||||
"columnName": "TITLE_JPN",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "thumbKey",
|
||||
"columnName": "THUMB",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "category",
|
||||
"columnName": "CATEGORY",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "posted",
|
||||
"columnName": "POSTED",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "uploader",
|
||||
"columnName": "UPLOADER",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "rating",
|
||||
"columnName": "RATING",
|
||||
"affinity": "REAL",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "simpleTags",
|
||||
"columnName": "SIMPLE_TAGS",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "pages",
|
||||
"columnName": "PAGES",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
},
|
||||
{
|
||||
"fieldPath": "simpleLanguage",
|
||||
"columnName": "SIMPLE_LANGUAGE",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "favoriteSlot",
|
||||
"columnName": "FAVORITE_SLOT",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"GID"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "DOWNLOAD_LABELS",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`LABEL` TEXT NOT NULL, `POSITION` INTEGER NOT NULL, `_id` INTEGER, PRIMARY KEY(`_id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "label",
|
||||
"columnName": "LABEL",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "position",
|
||||
"columnName": "POSITION",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "_id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"_id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_DOWNLOAD_LABELS_LABEL",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"LABEL"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_DOWNLOAD_LABELS_LABEL` ON `${TABLE_NAME}` (`LABEL`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "DOWNLOADS",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`GID` INTEGER NOT NULL, `STATE` INTEGER NOT NULL, `LEGACY` INTEGER NOT NULL, `TIME` INTEGER NOT NULL, `LABEL` TEXT, PRIMARY KEY(`GID`), FOREIGN KEY(`GID`) REFERENCES `GALLERIES`(`GID`) ON UPDATE NO ACTION ON DELETE NO ACTION , FOREIGN KEY(`LABEL`) REFERENCES `DOWNLOAD_LABELS`(`LABEL`) ON UPDATE CASCADE ON DELETE SET NULL )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "gid",
|
||||
"columnName": "GID",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "state",
|
||||
"columnName": "STATE",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "legacy",
|
||||
"columnName": "LEGACY",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "time",
|
||||
"columnName": "TIME",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "label",
|
||||
"columnName": "LABEL",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"GID"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_DOWNLOADS_LABEL",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"LABEL"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_DOWNLOADS_LABEL` ON `${TABLE_NAME}` (`LABEL`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "GALLERIES",
|
||||
"onDelete": "NO ACTION",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"GID"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"GID"
|
||||
]
|
||||
},
|
||||
{
|
||||
"table": "DOWNLOAD_LABELS",
|
||||
"onDelete": "SET NULL",
|
||||
"onUpdate": "CASCADE",
|
||||
"columns": [
|
||||
"LABEL"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"LABEL"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "DOWNLOAD_DIRNAME",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`GID` INTEGER NOT NULL, `DIRNAME` TEXT NOT NULL, PRIMARY KEY(`GID`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "gid",
|
||||
"columnName": "GID",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "dirname",
|
||||
"columnName": "DIRNAME",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"GID"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "DOWNLOAD_ARTISTS",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`GID` INTEGER NOT NULL, `ARTIST` TEXT NOT NULL, PRIMARY KEY(`GID`, `ARTIST`), FOREIGN KEY(`GID`) REFERENCES `DOWNLOADS`(`GID`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "gid",
|
||||
"columnName": "GID",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "artist",
|
||||
"columnName": "ARTIST",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"GID",
|
||||
"ARTIST"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "DOWNLOADS",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"GID"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"GID"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "FILTER",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`MODE` INTEGER NOT NULL, `TEXT` TEXT NOT NULL, `ENABLE` INTEGER NOT NULL, `_id` INTEGER, PRIMARY KEY(`_id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "mode",
|
||||
"columnName": "MODE",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "text",
|
||||
"columnName": "TEXT",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "enable",
|
||||
"columnName": "ENABLE",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "_id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"_id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "HISTORY",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`GID` INTEGER NOT NULL, `TIME` INTEGER NOT NULL, PRIMARY KEY(`GID`), FOREIGN KEY(`GID`) REFERENCES `GALLERIES`(`GID`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "gid",
|
||||
"columnName": "GID",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "time",
|
||||
"columnName": "TIME",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"GID"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "GALLERIES",
|
||||
"onDelete": "NO ACTION",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"GID"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"GID"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "LOCAL_FAVORITES",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`GID` INTEGER NOT NULL, `TIME` INTEGER NOT NULL, PRIMARY KEY(`GID`), FOREIGN KEY(`GID`) REFERENCES `GALLERIES`(`GID`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "gid",
|
||||
"columnName": "GID",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "time",
|
||||
"columnName": "TIME",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"GID"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "GALLERIES",
|
||||
"onDelete": "NO ACTION",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"GID"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"GID"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "PROGRESS",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`GID` INTEGER NOT NULL, `PAGE` INTEGER NOT NULL, PRIMARY KEY(`GID`), FOREIGN KEY(`GID`) REFERENCES `GALLERIES`(`GID`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "gid",
|
||||
"columnName": "GID",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "page",
|
||||
"columnName": "PAGE",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"GID"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "GALLERIES",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"GID"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"GID"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "QUICK_SEARCH",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER, `NAME` TEXT NOT NULL, `MODE` INTEGER NOT NULL, `CATEGORY` INTEGER NOT NULL, `KEYWORD` TEXT, `ADVANCE_SEARCH` INTEGER NOT NULL, `MIN_RATING` INTEGER NOT NULL, `PAGE_FROM` INTEGER NOT NULL, `PAGE_TO` INTEGER NOT NULL, `POSITION` INTEGER NOT NULL, PRIMARY KEY(`_id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "_id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "NAME",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "mode",
|
||||
"columnName": "MODE",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "category",
|
||||
"columnName": "CATEGORY",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "keyword",
|
||||
"columnName": "KEYWORD",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "advanceSearch",
|
||||
"columnName": "ADVANCE_SEARCH",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "minRating",
|
||||
"columnName": "MIN_RATING",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "pageFrom",
|
||||
"columnName": "PAGE_FROM",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "pageTo",
|
||||
"columnName": "PAGE_TO",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "position",
|
||||
"columnName": "POSITION",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"_id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"ftsVersion": "FTS4",
|
||||
"ftsOptions": {
|
||||
"tokenizer": "simple",
|
||||
"tokenizerArgs": [],
|
||||
"contentTable": "GALLERIES",
|
||||
"languageIdColumnName": "",
|
||||
"matchInfo": "FTS4",
|
||||
"notIndexedColumns": [],
|
||||
"prefixSizes": [],
|
||||
"preferredOrder": "ASC"
|
||||
},
|
||||
"contentSyncTriggers": [
|
||||
"CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_GALLERIES_FTS_BEFORE_UPDATE BEFORE UPDATE ON `GALLERIES` BEGIN DELETE FROM `GALLERIES_FTS` WHERE `docid`=OLD.`rowid`; END",
|
||||
"CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_GALLERIES_FTS_BEFORE_DELETE BEFORE DELETE ON `GALLERIES` BEGIN DELETE FROM `GALLERIES_FTS` WHERE `docid`=OLD.`rowid`; END",
|
||||
"CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_GALLERIES_FTS_AFTER_UPDATE AFTER UPDATE ON `GALLERIES` BEGIN INSERT INTO `GALLERIES_FTS`(`docid`, `TITLE`, `TITLE_JPN`, `SIMPLE_TAGS`) VALUES (NEW.`rowid`, NEW.`TITLE`, NEW.`TITLE_JPN`, NEW.`SIMPLE_TAGS`); END",
|
||||
"CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_GALLERIES_FTS_AFTER_INSERT AFTER INSERT ON `GALLERIES` BEGIN INSERT INTO `GALLERIES_FTS`(`docid`, `TITLE`, `TITLE_JPN`, `SIMPLE_TAGS`) VALUES (NEW.`rowid`, NEW.`TITLE`, NEW.`TITLE_JPN`, NEW.`SIMPLE_TAGS`); END"
|
||||
],
|
||||
"tableName": "GALLERIES_FTS",
|
||||
"createSql": "CREATE VIRTUAL TABLE IF NOT EXISTS `${TABLE_NAME}` USING FTS4(`TITLE` TEXT NOT NULL, `TITLE_JPN` TEXT NOT NULL, `SIMPLE_TAGS` TEXT, content=`GALLERIES`)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "title",
|
||||
"columnName": "TITLE",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "titleJpn",
|
||||
"columnName": "TITLE_JPN",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "simpleTags",
|
||||
"columnName": "SIMPLE_TAGS",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": []
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '7d1cfc4e9a9d506a73eb0520ae414d25')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,6 @@
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:largeHeap="@bool/large_heap"
|
||||
android:localeConfig="@xml/locale_config"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
@@ -71,6 +70,39 @@
|
||||
<data android:host="exhentai.org" />
|
||||
<data android:host="e-hentai.org" />
|
||||
</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" />
|
||||
|
||||
<data android:scheme="file" />
|
||||
<data android:scheme="content" />
|
||||
<data android:host="*" />
|
||||
<data android:mimeType="application/octet-stream" />
|
||||
<data android:mimeType="application/zip" />
|
||||
<data android:mimeType="application/x-7z-compressed" />
|
||||
<data android:mimeType="application/x-tar" />
|
||||
<data android:mimeType="application/x-xz" />
|
||||
<data android:mimeType="application/gzip" />
|
||||
<data android:mimeType="application/vnd.rar" />
|
||||
<data android:mimeType="application/vnd.comicbook+zip" />
|
||||
<data android:mimeType="application/vnd.comicbook-rar" />
|
||||
|
||||
<!-- unofficial -->
|
||||
<data android:mimeType="application/7z" />
|
||||
<data android:mimeType="application/rar" />
|
||||
<data android:mimeType="application/x-cbz" />
|
||||
<data android:mimeType="application/x-cbr" />
|
||||
<data android:mimeType="application/x-cbt" />
|
||||
<data android:mimeType="application/x-cb7" />
|
||||
<data android:mimeType="application/x-zip-compressed" />
|
||||
<data android:mimeType="application/x-rar-compressed" />
|
||||
<data android:mimeType="application/x-compressed-tar" />
|
||||
<data android:mimeType="application/x-bzip-compressed-tar" />
|
||||
<data android:mimeType="application/x-lzma-compressed-tar" />
|
||||
<data android:mimeType="application/x-xz-compressed-tar" />
|
||||
</intent-filter>
|
||||
<intent-filter android:label="@string/keyword_search">
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
|
||||
@@ -86,44 +118,6 @@
|
||||
<data android:mimeType="image/*" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name="eu.kanade.tachiyomi.ui.reader.ReaderActivity"
|
||||
android:configChanges="screenSize"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTask">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="file" />
|
||||
<data android:scheme="content" />
|
||||
<data android:host="*" />
|
||||
<data android:mimeType="application/octet-stream" />
|
||||
<data android:mimeType="application/7z" />
|
||||
<data android:mimeType="application/rar" />
|
||||
<data android:mimeType="application/zip" />
|
||||
<data android:mimeType="application/x-7z-compressed" />
|
||||
<data android:mimeType="application/x-rar-compressed" />
|
||||
<data android:mimeType="application/x-zip-compressed" />
|
||||
<data android:mimeType="application/x-tar" />
|
||||
<data android:mimeType="application/x-xz" />
|
||||
<data android:mimeType="application/gzip" />
|
||||
<data android:mimeType="application/x-cbz" />
|
||||
<data android:mimeType="application/x-cbr" />
|
||||
<data android:mimeType="application/x-cbt" />
|
||||
<data android:mimeType="application/x-cb7" />
|
||||
|
||||
<!-- untested -->
|
||||
<data android:mimeType="application/vnd.comicbook+zip" />
|
||||
<data android:mimeType="application/vnd.comicbook-rar" />
|
||||
<data android:mimeType="application/x-compressed-tar" />
|
||||
<data android:mimeType="application/x-bzip-compressed-tar" />
|
||||
<data android:mimeType="application/x-lzma-compressed-tar" />
|
||||
<data android:mimeType="application/x-xz-compressed-tar" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ui.SecurityActivity"
|
||||
android:configChanges="screenSize"
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
From aee8f7147ddb262fd9e5feee8d5b17094cf3470f Mon Sep 17 00:00:00 2001
|
||||
From: FooIbar <118464521+FooIbar@users.noreply.github.com>
|
||||
Date: Tue, 26 Sep 2023 00:09:25 +0800
|
||||
Subject: [PATCH] Insert link libs
|
||||
|
||||
---
|
||||
CMakeLists.txt | 2 +-
|
||||
1 file changed, 1 insertion(+), 1 deletion(-)
|
||||
|
||||
diff --git a/CMakeLists.txt b/CMakeLists.txt
|
||||
index ec97e4c7..420f204c 100644
|
||||
--- a/CMakeLists.txt
|
||||
+++ b/CMakeLists.txt
|
||||
@@ -438,7 +438,7 @@ IF(DEFINED __GNUWIN32PATH AND EXISTS "${__GNUWIN32PATH}")
|
||||
# # endif
|
||||
ENDIF(DEFINED __GNUWIN32PATH AND EXISTS "${__GNUWIN32PATH}")
|
||||
|
||||
-SET(ADDITIONAL_LIBS "")
|
||||
+SET(ADDITIONAL_LIBS ${LIBARCHIVE_CUSTOM_LIBS})
|
||||
#
|
||||
# Find ZLIB
|
||||
#
|
||||
--
|
||||
2.34.1
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
cmake_minimum_required(VERSION 3.14)
|
||||
project(ehviewer C)
|
||||
include(ExternalProject)
|
||||
include(FetchContent)
|
||||
|
||||
if (NOT CMAKE_BUILD_TYPE STREQUAL "Debug")
|
||||
@@ -18,31 +19,48 @@ endif (NOT CMAKE_BUILD_TYPE STREQUAL "Debug")
|
||||
|
||||
option(BUILD_TESTING OFF)
|
||||
|
||||
set(XZ_PATCH ${CMAKE_CURRENT_LIST_DIR}/0001-Don-t-build-other-binaries.patch)
|
||||
FetchContent_Declare(
|
||||
liblzma
|
||||
GIT_REPOSITORY https://git.tukaani.org/xz.git
|
||||
GIT_TAG v5.6.1
|
||||
PATCH_COMMAND git apply ${CMAKE_CURRENT_LIST_DIR}/0001-Don-t-build-other-binaries.patch || cd .
|
||||
GIT_REPOSITORY https://github.com/tukaani-project/xz.git
|
||||
GIT_TAG v5.6.2
|
||||
PATCH_COMMAND git apply --check -R ${XZ_PATCH} || git apply ${XZ_PATCH}
|
||||
)
|
||||
|
||||
FetchContent_MakeAvailable(liblzma)
|
||||
include_directories(${liblzma_SOURCE_DIR}/src/liblzma/api)
|
||||
|
||||
# Build GNUTLS libnettle
|
||||
FetchContent_Declare(
|
||||
if (CMAKE_HOST_SYSTEM_NAME STREQUAL Windows)
|
||||
execute_process(
|
||||
COMMAND wsl wslpath ${CMAKE_CURRENT_LIST_DIR}
|
||||
OUTPUT_VARIABLE CURRENT_DIR
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||
)
|
||||
else ()
|
||||
set(CURRENT_DIR ${CMAKE_CURRENT_LIST_DIR})
|
||||
endif ()
|
||||
set(NDK_VERSION "${ANDROID_NDK_MAJOR}.${ANDROID_NDK_MINOR}.${ANDROID_NDK_BUILD}")
|
||||
ExternalProject_Add(
|
||||
nettle
|
||||
URL https://ftp.gnu.org/gnu/nettle/nettle-3.9.1.tar.gz
|
||||
URL_MD5 29fcd2dec6bf5b48e5e3ffb3cbc4779e
|
||||
SOURCE_DIR nettle-src/nettle
|
||||
PREFIX nettle
|
||||
GIT_REPOSITORY https://gitlab.com/gnutls/nettle.git
|
||||
GIT_TAG nettle_3.10_release_20240616
|
||||
GIT_CONFIG core.autocrlf=input
|
||||
CONFIGURE_COMMAND bash ${CURRENT_DIR}/configure_nettle.sh ${NDK_VERSION} ${CMAKE_C_COMPILER_TARGET}
|
||||
BUILD_COMMAND bash -c "make libnettle.a CFLAGS='${CMAKE_C_FLAGS}'"
|
||||
INSTALL_COMMAND ""
|
||||
BUILD_IN_SOURCE 1
|
||||
BUILD_BYPRODUCTS nettle/src/nettle/libnettle.a
|
||||
)
|
||||
|
||||
FetchContent_MakeAvailable(nettle)
|
||||
set(NETTLE_DIR ${nettle_SOURCE_DIR})
|
||||
add_subdirectory(nettle)
|
||||
ExternalProject_Get_Property(nettle SOURCE_DIR)
|
||||
include_directories(${SOURCE_DIR}/..)
|
||||
add_library(libnettle STATIC IMPORTED)
|
||||
set_property(TARGET libnettle PROPERTY IMPORTED_LOCATION ${SOURCE_DIR}/libnettle.a)
|
||||
add_dependencies(libnettle nettle)
|
||||
|
||||
# Configure libnettle support for libarchive
|
||||
include_directories(${CMAKE_CURRENT_BINARY_DIR}/nettle-src)
|
||||
include_directories(.)
|
||||
set(HAVE_LIBNETTLE 1)
|
||||
set(HAVE_NETTLE_AES_H 1)
|
||||
set(HAVE_NETTLE_HMAC_H 1)
|
||||
@@ -58,6 +76,7 @@ SET(HAVE_LZMA_STREAM_ENCODER_MT 1)
|
||||
SET(HAVE_LZMADEC_H 1)
|
||||
SET(HAVE_LIBLZMADEC 1)
|
||||
|
||||
option(ENABLE_OPENSSL OFF)
|
||||
option(ENABLE_TAR OFF)
|
||||
option(ENABLE_CPIO OFF)
|
||||
option(ENABLE_CAT OFF)
|
||||
@@ -65,12 +84,14 @@ option(ENABLE_UNZIP OFF)
|
||||
option(ENABLE_TEST OFF)
|
||||
|
||||
# Configure libarchive link's static lib
|
||||
SET(LIBARCHIVE_CUSTOM_LIBS "nettle" "liblzma")
|
||||
SET(LIBARCHIVE_CUSTOM_LIBS "libnettle" "liblzma")
|
||||
|
||||
set(LIBARCHIVE_PATCH ${CMAKE_CURRENT_LIST_DIR}/0001-Insert-link-libs.patch)
|
||||
FetchContent_Declare(
|
||||
libarchive
|
||||
GIT_REPOSITORY https://github.com/FooIbar/libarchive.git
|
||||
GIT_TAG v3.7.2
|
||||
GIT_REPOSITORY https://github.com/libarchive/libarchive.git
|
||||
GIT_TAG v3.7.6
|
||||
PATCH_COMMAND git apply --check -R ${LIBARCHIVE_PATCH} || git apply ${LIBARCHIVE_PATCH}
|
||||
)
|
||||
|
||||
FetchContent_MakeAvailable(libarchive)
|
||||
@@ -79,12 +100,16 @@ include_directories(${libarchive_SOURCE_DIR}/libarchive)
|
||||
FetchContent_Declare(
|
||||
Corrosion
|
||||
GIT_REPOSITORY https://github.com/corrosion-rs/corrosion.git
|
||||
GIT_TAG 0a3bdf452995803c334d79dc9a2affbfad51b720
|
||||
GIT_TAG v0.5
|
||||
)
|
||||
|
||||
FetchContent_MakeAvailable(Corrosion)
|
||||
corrosion_import_crate(MANIFEST_PATH ../rust/Cargo.toml)
|
||||
corrosion_add_target_local_rustflags(ehviewer_rust "-Clink-args=-Wl,-z,max-page-size=16384")
|
||||
if (ANDROID_NATIVE_API_LEVEL GREATER_EQUAL 26)
|
||||
corrosion_set_features(ehviewer_rust FEATURES api-level-26)
|
||||
endif ()
|
||||
|
||||
# Build and link our app's native lib
|
||||
add_library(${PROJECT_NAME} SHARED archive.c border.c gifutils.c natsort/strnatcmp.c)
|
||||
target_link_libraries(${PROJECT_NAME} ehviewer_rust archive_static log jnigraphics)
|
||||
add_library(${PROJECT_NAME} SHARED archive.c gifutils.c hash.c natsort/strnatcmp.c)
|
||||
target_link_libraries(${PROJECT_NAME} ehviewer_rust archive_static log jnigraphics -Wl,--exclude-libs,ALL)
|
||||
+42
-33
@@ -49,23 +49,24 @@ typedef struct {
|
||||
} entry;
|
||||
|
||||
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
static archive_ctx **ctx_pool;
|
||||
static archive_ctx **ctx_pool = NULL;
|
||||
#define CTX_POOL_SIZE 20
|
||||
|
||||
static void *mempool = NULL;
|
||||
static void *mempool = MAP_FAILED;
|
||||
static size_t *mempoolofs = NULL;
|
||||
static int page_size = 0;
|
||||
|
||||
#define PAGE_ALIGN(x) ((x + ~PAGE_MASK) & PAGE_MASK)
|
||||
#define PAGE_ALIGN(x) ((x + page_size - 1) & ~(page_size - 1))
|
||||
|
||||
#define MEMPOOL_ADDR_BY_SORTED_IDX(x) (mempool + (index ? mempoolofs[index - 1] : 0))
|
||||
#define MEMPOOL_SIZE (mempoolofs[entryCount - 1])
|
||||
|
||||
#define PROT_RW (PROT_WRITE | PROT_READ)
|
||||
#define MAP_ANON_POOL (MAP_ANONYMOUS | MAP_NORESERVE | MAP_PRIVATE | MAP_UNINITIALIZED)
|
||||
#define MAP_ANON_POOL (MAP_ANONYMOUS | MAP_NORESERVE | MAP_PRIVATE)
|
||||
|
||||
static bool need_encrypt = false;
|
||||
static char *passwd = NULL;
|
||||
static void *archiveAddr = NULL;
|
||||
static void *archiveAddr = MAP_FAILED;
|
||||
static size_t archiveSize = 0;
|
||||
static entry *entries = NULL;
|
||||
static size_t entryCount = 0;
|
||||
@@ -116,8 +117,8 @@ static inline int compare_entries(const void *a, const void *b) {
|
||||
return strnatcmp(fa, fb);
|
||||
}
|
||||
|
||||
static long archive_map_entries_index(archive_ctx *ctx, bool sort) {
|
||||
long count = 0;
|
||||
static int archive_map_entries_index(archive_ctx *ctx, bool sort) {
|
||||
int count = 0;
|
||||
while (archive_read_next_header(ctx->arc, &ctx->entry) == ARCHIVE_OK) {
|
||||
const char *name = archive_entry_pathname2(ctx->entry);
|
||||
if (archive_entry_is_file(ctx->entry) && filename_is_playable_file(name)) {
|
||||
@@ -164,8 +165,8 @@ static void mempool_prefault_pages(void *addr, size_t size) {
|
||||
}
|
||||
}
|
||||
|
||||
static long archive_list_all_entries(archive_ctx *ctx) {
|
||||
long count = 0;
|
||||
static int archive_list_all_entries(archive_ctx *ctx) {
|
||||
int count = 0;
|
||||
while (archive_read_next_header(ctx->arc, &ctx->entry) == ARCHIVE_OK)
|
||||
if (archive_entry_is_playable(ctx->entry))
|
||||
count++;
|
||||
@@ -282,6 +283,7 @@ JNIEXPORT jint JNICALL
|
||||
Java_com_hippo_ehviewer_jni_ArchiveKt_openArchive(JNIEnv *env, jclass thiz, jint fd, jlong size, jboolean sort_entries) {
|
||||
EH_UNUSED(env);
|
||||
EH_UNUSED(thiz);
|
||||
page_size = getpagesize();
|
||||
archive_ctx *ctx = NULL;
|
||||
archiveAddr = mmap(0, size, PROT_READ, MAP_PRIVATE, fd, 0);
|
||||
if (archiveAddr == MAP_FAILED) {
|
||||
@@ -294,11 +296,12 @@ Java_com_hippo_ehviewer_jni_ArchiveKt_openArchive(JNIEnv *env, jclass thiz, jint
|
||||
LOGE("Allocate archive ctx pool failed:ENOMEM");
|
||||
return 0;
|
||||
}
|
||||
long r = archive_alloc_ctx(&ctx);
|
||||
int r = archive_alloc_ctx(&ctx);
|
||||
if (r) {
|
||||
if (r == -ENOMEM)
|
||||
LOGE("%s", "Mem alloc failed");
|
||||
LOGE("%s%s", "Archive open failed:", archive_error_string(ctx->arc));
|
||||
return 0;
|
||||
} else {
|
||||
entryCount = archive_list_all_entries(ctx);
|
||||
LOGI("%s%zu%s", "Found ", entryCount, " image entries in archive");
|
||||
@@ -345,10 +348,6 @@ Java_com_hippo_ehviewer_jni_ArchiveKt_openArchive(JNIEnv *env, jclass thiz, jint
|
||||
r = archive_map_entries_index(ctx, sort_entries);
|
||||
if (!archive_prealloc_mempool()) {
|
||||
r = 0;
|
||||
for (int i = 0; i < entryCount; ++i) {
|
||||
free((void *) entries[i].filename);
|
||||
}
|
||||
free(entries);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -401,19 +400,32 @@ JNIEXPORT void JNICALL
|
||||
Java_com_hippo_ehviewer_jni_ArchiveKt_closeArchive(JNIEnv *env, jclass thiz) {
|
||||
EH_UNUSED(env);
|
||||
EH_UNUSED(thiz);
|
||||
for (int i = 0; i < CTX_POOL_SIZE; i++)
|
||||
archive_release_ctx(ctx_pool[i]);
|
||||
if (ctx_pool) {
|
||||
for (int i = 0; i < CTX_POOL_SIZE; i++)
|
||||
archive_release_ctx(ctx_pool[i]);
|
||||
free(ctx_pool);
|
||||
ctx_pool = NULL;
|
||||
}
|
||||
free(passwd);
|
||||
free(ctx_pool);
|
||||
passwd = NULL;
|
||||
need_encrypt = false;
|
||||
munmap(archiveAddr, archiveSize);
|
||||
munmap(mempool, MEMPOOL_SIZE);
|
||||
if (archiveAddr != MAP_FAILED) {
|
||||
munmap(archiveAddr, archiveSize);
|
||||
archiveAddr = MAP_FAILED;
|
||||
}
|
||||
if (mempool != MAP_FAILED) {
|
||||
munmap(mempool, MEMPOOL_SIZE);
|
||||
mempool = MAP_FAILED;
|
||||
}
|
||||
free(mempoolofs);
|
||||
for (int i = 0; i < entryCount; ++i) {
|
||||
free((void *) entries[i].filename);
|
||||
mempoolofs = NULL;
|
||||
if (entries) {
|
||||
for (int i = 0; i < entryCount; ++i) {
|
||||
free((void *) entries[i].filename);
|
||||
}
|
||||
free(entries);
|
||||
entries = NULL;
|
||||
}
|
||||
free(entries);
|
||||
}
|
||||
|
||||
JNIEXPORT jboolean JNICALL
|
||||
@@ -430,12 +442,9 @@ Java_com_hippo_ehviewer_jni_ArchiveKt_providePassword(JNIEnv *env, jclass thiz,
|
||||
archive_ctx *ctx;
|
||||
jboolean ret = true;
|
||||
int len = (*env)->GetStringUTFLength(env, str);
|
||||
passwd = realloc(passwd, len * sizeof(char));
|
||||
if (!passwd) {
|
||||
LOGE("Allocate passwd buffer failed");
|
||||
return false;
|
||||
}
|
||||
passwd = realloc(passwd, len + 1);
|
||||
(*env)->GetStringUTFRegion(env, str, 0, len, passwd);
|
||||
passwd[len] = 0;
|
||||
archive_alloc_ctx(&ctx);
|
||||
void *tmpBuf = alloca(4096);
|
||||
while (archive_read_next_header(ctx->arc, &entry) == ARCHIVE_OK) {
|
||||
@@ -484,7 +493,7 @@ Java_com_hippo_ehviewer_jni_ArchiveKt_releaseByteBuffer(JNIEnv *env, jclass thiz
|
||||
if (!ADDR_IN_FILE_MAPPING(addr)) mempool_release_pages(addr, size);
|
||||
}
|
||||
|
||||
JNIEXPORT jboolean JNICALL
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_hippo_ehviewer_jni_ArchiveKt_archiveFdBatch(JNIEnv *env, jclass clazz, jintArray fd_batch, jobjectArray names, jint arc_fd, jint size) {
|
||||
EH_UNUSED(clazz);
|
||||
struct archive *arc = archive_write_new();
|
||||
@@ -506,15 +515,15 @@ Java_com_hippo_ehviewer_jni_ArchiveKt_archiveFdBatch(JNIEnv *env, jclass clazz,
|
||||
archive_entry_copy_stat(entry, &st);
|
||||
archive_entry_set_perm(entry, 0644);
|
||||
archive_write_header(arc, entry);
|
||||
int len = read(fd, buff, sizeof(buff));
|
||||
while (len > 0) {
|
||||
archive_write_data(arc, buff, len);
|
||||
int len;
|
||||
do {
|
||||
len = read(fd, buff, sizeof(buff));
|
||||
}
|
||||
archive_write_data(arc, buff, len);
|
||||
} while (len > 0);
|
||||
archive_write_finish_entry(arc);
|
||||
archive_entry_clear(entry);
|
||||
}
|
||||
archive_entry_free(entry);
|
||||
archive_write_close(arc);
|
||||
archive_write_free(arc);
|
||||
return true;
|
||||
}
|
||||
@@ -1,241 +0,0 @@
|
||||
#include <jni.h>
|
||||
#include <math.h>
|
||||
#include <stdbool.h>
|
||||
#include <android/bitmap.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
/** A line will be considered as having content if 0.25% of it is filled. */
|
||||
const float filledRatioLimit = 0.0025;
|
||||
|
||||
/** When the threshold is closer to 1, less content will be cropped. **/
|
||||
#define THRESHOLD 0.75
|
||||
|
||||
const uint8_t thresholdForBlack = (uint8_t) (255.0 * THRESHOLD);
|
||||
|
||||
const uint8_t thresholdForWhite = (uint8_t) (255.0 - 255.0 * THRESHOLD);
|
||||
|
||||
static uint8_t inline grey(uint32_t pixel) {
|
||||
const uint8_t r = ((uint8_t *) &pixel)[0];
|
||||
const uint8_t g = ((uint8_t *) &pixel)[1];
|
||||
const uint8_t b = ((uint8_t *) &pixel)[2];
|
||||
return (r * 77 + g * 151 + b * 28) >> 8;
|
||||
}
|
||||
|
||||
static bool inline isBlackPixel(const uint32_t *pixels, uint32_t width, uint32_t x, uint32_t y) {
|
||||
const uint32_t pixel = *((uint32_t *) pixels + (y * width + x));
|
||||
return grey(pixel) < thresholdForBlack;
|
||||
}
|
||||
|
||||
static bool inline isWhitePixel(const uint32_t *pixels, uint32_t width, uint32_t x, uint32_t y) {
|
||||
const uint32_t pixel = *((uint32_t *) pixels + (y * width + x));
|
||||
return grey(pixel) > thresholdForWhite;
|
||||
}
|
||||
|
||||
/** Return the first x position where there is a substantial amount of fill,
|
||||
* starting the search from the left. */
|
||||
uint32_t findBorderLeft(uint32_t *pixels, uint32_t width, uint32_t height,
|
||||
uint32_t top, uint32_t bottom) {
|
||||
int x, y;
|
||||
const uint32_t filledLimit = round(height * filledRatioLimit / 2);
|
||||
|
||||
// Scan first line to detect dominant color
|
||||
uint32_t whitePixels = 0;
|
||||
uint32_t blackPixels = 0;
|
||||
|
||||
for (y = top; y < bottom; y += 2) {
|
||||
if (isBlackPixel(pixels, width, 0, y)) {
|
||||
blackPixels++;
|
||||
} else if (isWhitePixel(pixels, width, 0, y)) {
|
||||
whitePixels++;
|
||||
}
|
||||
}
|
||||
|
||||
bool (*detectFunc)(const uint32_t *, uint32_t, uint32_t, uint32_t) = isBlackPixel;
|
||||
if (whitePixels > filledLimit && blackPixels > filledLimit) {
|
||||
// Mixed fill found... don't crop anything
|
||||
return 0;
|
||||
} else if (blackPixels > filledLimit) {
|
||||
detectFunc = isWhitePixel;
|
||||
}
|
||||
|
||||
// Scan vertical lines in search of filled lines
|
||||
for (x = 1; x < width; x++) {
|
||||
uint32_t filledCount = 0;
|
||||
|
||||
for (y = top; y < bottom; y += 2) {
|
||||
if (detectFunc(pixels, width, x, y)) {
|
||||
filledCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (filledCount > filledLimit) {
|
||||
// This line contains enough fill
|
||||
return x;
|
||||
}
|
||||
}
|
||||
|
||||
// No fill found... don't crop anything
|
||||
return 0;
|
||||
}
|
||||
|
||||
/** Return the first x position where there is a substantial amount of fill,
|
||||
* starting the search from the right. */
|
||||
uint32_t findBorderRight(uint32_t *pixels, uint32_t width, uint32_t height,
|
||||
uint32_t top, uint32_t bottom) {
|
||||
int x, y;
|
||||
const uint32_t filledLimit = round(height * filledRatioLimit / 2);
|
||||
|
||||
// Scan first line to detect dominant color
|
||||
uint32_t whitePixels = 0;
|
||||
uint32_t blackPixels = 0;
|
||||
|
||||
uint32_t lastX = width - 1;
|
||||
for (y = top; y < bottom; y += 2) {
|
||||
if (isBlackPixel(pixels, width, lastX, y)) {
|
||||
blackPixels++;
|
||||
} else if (isWhitePixel(pixels, width, lastX, y)) {
|
||||
whitePixels++;
|
||||
}
|
||||
}
|
||||
|
||||
bool (*detectFunc)(const uint32_t *, uint32_t, uint32_t, uint32_t) = isBlackPixel;
|
||||
if (whitePixels > filledLimit && blackPixels > filledLimit) {
|
||||
// Mixed fill found... don't crop anything
|
||||
return width;
|
||||
} else if (blackPixels > filledLimit) {
|
||||
detectFunc = isWhitePixel;
|
||||
}
|
||||
|
||||
// Scan vertical lines in search of filled lines
|
||||
for (x = width - 2; x > 0; x--) {
|
||||
uint32_t filledCount = 0;
|
||||
|
||||
for (y = top; y < bottom; y += 2) {
|
||||
if (detectFunc(pixels, width, x, y)) {
|
||||
filledCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (filledCount > filledLimit) {
|
||||
// This line contains enough fill
|
||||
return x + 1;
|
||||
}
|
||||
}
|
||||
|
||||
// No fill found... don't crop anything
|
||||
return width;
|
||||
}
|
||||
|
||||
/** Return the first y position where there is a substantial amount of fill,
|
||||
* starting the search from the top. */
|
||||
uint32_t findBorderTop(uint32_t *pixels, uint32_t width, uint32_t height) {
|
||||
int x, y;
|
||||
const uint32_t filledLimit = round(width * filledRatioLimit / 2);
|
||||
|
||||
// Scan first line to detect dominant color
|
||||
uint32_t whitePixels = 0;
|
||||
uint32_t blackPixels = 0;
|
||||
|
||||
for (x = 0; x < width; x += 2) {
|
||||
if (isBlackPixel(pixels, width, x, 0)) {
|
||||
blackPixels++;
|
||||
} else if (isWhitePixel(pixels, width, x, 0)) {
|
||||
whitePixels++;
|
||||
}
|
||||
}
|
||||
|
||||
bool (*detectFunc)(const uint32_t *, uint32_t, uint32_t, uint32_t) = isBlackPixel;
|
||||
if (whitePixels > filledLimit && blackPixels > filledLimit) {
|
||||
// Mixed fill found... don't crop anything
|
||||
return 0;
|
||||
} else if (blackPixels > filledLimit) {
|
||||
detectFunc = isWhitePixel;
|
||||
}
|
||||
|
||||
// Scan horizontal lines in search of filled lines
|
||||
for (y = 1; y < height; y++) {
|
||||
uint32_t filledCount = 0;
|
||||
|
||||
for (x = 0; x < width; x += 2) {
|
||||
if (detectFunc(pixels, width, x, y)) {
|
||||
filledCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (filledCount > filledLimit) {
|
||||
// This line contains enough fill
|
||||
return y;
|
||||
}
|
||||
}
|
||||
|
||||
// No fill found... don't crop anything
|
||||
return 0;
|
||||
}
|
||||
|
||||
/** Return the first y position where there is a substantial amount of fill,
|
||||
* starting the search from the bottom. */
|
||||
uint32_t findBorderBottom(uint32_t *pixels, uint32_t width, uint32_t height) {
|
||||
int x, y;
|
||||
const uint32_t filledLimit = round(width * filledRatioLimit / 2);
|
||||
|
||||
// Scan first line to detect dominant color
|
||||
uint32_t whitePixels = 0;
|
||||
uint32_t blackPixels = 0;
|
||||
uint32_t lastY = height - 1;
|
||||
|
||||
for (x = 0; x < width; x += 2) {
|
||||
if (isBlackPixel(pixels, width, x, lastY)) {
|
||||
blackPixels++;
|
||||
} else if (isWhitePixel(pixels, width, x, lastY)) {
|
||||
whitePixels++;
|
||||
}
|
||||
}
|
||||
|
||||
bool (*detectFunc)(const uint32_t *, uint32_t, uint32_t, uint32_t) = isBlackPixel;
|
||||
if (whitePixels > filledLimit && blackPixels > filledLimit) {
|
||||
// Mixed fill found... don't crop anything
|
||||
return height;
|
||||
} else if (blackPixels > filledLimit) {
|
||||
detectFunc = isWhitePixel;
|
||||
}
|
||||
|
||||
// Scan horizontal lines in search of filled lines
|
||||
for (y = height - 2; y > 0; y--) {
|
||||
uint32_t filledCount = 0;
|
||||
|
||||
for (x = 0; x < width; x += 2) {
|
||||
if (detectFunc(pixels, width, x, y)) {
|
||||
filledCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (filledCount > filledLimit) {
|
||||
// This line contains enough fill
|
||||
return y + 1;
|
||||
}
|
||||
}
|
||||
|
||||
// No fill found... don't crop anything
|
||||
return height;
|
||||
}
|
||||
|
||||
JNIEXPORT jintArray JNICALL
|
||||
Java_com_hippo_ehviewer_image_ImageKt_detectBorder(JNIEnv *env, jclass clazz, jobject bitmap) {
|
||||
void *pixels = NULL;
|
||||
AndroidBitmapInfo info;
|
||||
memset(&info, 0, sizeof(info));
|
||||
AndroidBitmap_getInfo(env, bitmap, &info);
|
||||
AndroidBitmap_lockPixels(env, bitmap, &pixels);
|
||||
uint32_t width = info.width;
|
||||
uint32_t height = info.height;
|
||||
uint32_t top = findBorderTop(pixels, width, height);
|
||||
uint32_t bottom = findBorderBottom(pixels, width, height);
|
||||
uint32_t left = findBorderLeft(pixels, width, height, top, bottom);
|
||||
uint32_t right = findBorderRight(pixels, width, height, top, bottom);
|
||||
int array[4] = {left, top, right, bottom};
|
||||
AndroidBitmap_unlockPixels(env, bitmap);
|
||||
jintArray ret = (*env)->NewIntArray(env, 4);
|
||||
(*env)->SetIntArrayRegion(env, ret, 0, 4, array);
|
||||
return ret;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
SUFFIX=toolchains/llvm/prebuilt/$(uname -s | tr '[:upper:]' '[:lower:]')-x86_64/bin
|
||||
CI_NDK_LOCATION=$ANDROID_HOME/ndk/$1/$SUFFIX
|
||||
ARCH_NDK_LOCATION=$ANDROID_HOME/ndk-bundle/$SUFFIX
|
||||
if test -e $CI_NDK_LOCATION ;then
|
||||
TOOLCHAIN=$CI_NDK_LOCATION
|
||||
else
|
||||
TOOLCHAIN=$ARCH_NDK_LOCATION
|
||||
fi
|
||||
TARGET=$2
|
||||
./.bootstrap && ./configure --host=$TARGET --disable-public-key --disable-dependency-tracking \
|
||||
AR=$TOOLCHAIN/llvm-ar \
|
||||
CC="$TOOLCHAIN/clang -target $TARGET" \
|
||||
RANLIB=$TOOLCHAIN/llvm-ranlib
|
||||
@@ -0,0 +1,39 @@
|
||||
#include <unistd.h>
|
||||
|
||||
#include <jni.h>
|
||||
#include <nettle/sha1.h>
|
||||
|
||||
#include "ehviewer.h"
|
||||
|
||||
#define BUFFER_SIZE 8192
|
||||
|
||||
typedef uint8_t byte;
|
||||
|
||||
const char hex_digits[] = "0123456789abcdef";
|
||||
|
||||
JNIEXPORT jstring JNICALL
|
||||
Java_com_hippo_ehviewer_jni_HashKt_sha1(JNIEnv *env, jclass clazz, jint fd) {
|
||||
EH_UNUSED(clazz);
|
||||
struct sha1_ctx ctx;
|
||||
sha1_init(&ctx);
|
||||
|
||||
size_t bytes_read;
|
||||
byte buffer[BUFFER_SIZE];
|
||||
while ((bytes_read = read(fd, buffer, BUFFER_SIZE)) > 0) {
|
||||
sha1_update(&ctx, bytes_read, buffer);
|
||||
}
|
||||
|
||||
byte digest[SHA1_DIGEST_SIZE];
|
||||
sha1_digest(&ctx, SHA1_DIGEST_SIZE, digest);
|
||||
|
||||
byte byte;
|
||||
char hex_digest[2 * SHA1_DIGEST_SIZE + 1];
|
||||
for (int i = 0; i < SHA1_DIGEST_SIZE; i++) {
|
||||
byte = digest[i];
|
||||
hex_digest[2 * i] = hex_digits[byte >> 4 & 0xF];
|
||||
hex_digest[2 * i + 1] = hex_digits[byte & 0xF];
|
||||
}
|
||||
hex_digest[2 * SHA1_DIGEST_SIZE] = '\0';
|
||||
|
||||
return (*env)->NewStringUTF(env, hex_digest);
|
||||
}
|
||||
@@ -1,238 +0,0 @@
|
||||
cmake_minimum_required(VERSION 3.4.1)
|
||||
project(nettle C)
|
||||
|
||||
set(LIBNETTLE_DEFINITIONS
|
||||
-DHAVE_CONFIG_H)
|
||||
|
||||
set(LIBNETTLE_SOURCES
|
||||
${NETTLE_DIR}/aes-decrypt-internal.c
|
||||
${NETTLE_DIR}/aes-decrypt.c
|
||||
${NETTLE_DIR}/aes-decrypt-table.c
|
||||
${NETTLE_DIR}/aes128-decrypt.c
|
||||
${NETTLE_DIR}/aes192-decrypt.c
|
||||
${NETTLE_DIR}/aes256-decrypt.c
|
||||
${NETTLE_DIR}/aes-encrypt-internal.c
|
||||
${NETTLE_DIR}/aes-encrypt.c
|
||||
${NETTLE_DIR}/aes-encrypt-table.c
|
||||
${NETTLE_DIR}/aes128-encrypt.c
|
||||
${NETTLE_DIR}/aes192-encrypt.c
|
||||
${NETTLE_DIR}/aes256-encrypt.c
|
||||
${NETTLE_DIR}/aes-invert-internal.c
|
||||
${NETTLE_DIR}/aes-set-key-internal.c
|
||||
${NETTLE_DIR}/aes-set-encrypt-key.c
|
||||
${NETTLE_DIR}/aes-set-decrypt-key.c
|
||||
${NETTLE_DIR}/aes128-set-encrypt-key.c
|
||||
${NETTLE_DIR}/aes128-set-decrypt-key.c
|
||||
${NETTLE_DIR}/aes128-meta.c
|
||||
${NETTLE_DIR}/aes192-set-encrypt-key.c
|
||||
${NETTLE_DIR}/aes192-set-decrypt-key.c
|
||||
${NETTLE_DIR}/aes192-meta.c
|
||||
${NETTLE_DIR}/aes256-set-encrypt-key.c
|
||||
${NETTLE_DIR}/aes256-set-decrypt-key.c
|
||||
${NETTLE_DIR}/aes256-meta.c
|
||||
${NETTLE_DIR}/nist-keywrap.c
|
||||
${NETTLE_DIR}/arcfour.c
|
||||
${NETTLE_DIR}/arctwo.c
|
||||
${NETTLE_DIR}/arctwo-meta.c
|
||||
${NETTLE_DIR}/blowfish.c
|
||||
${NETTLE_DIR}/blowfish-bcrypt.c
|
||||
${NETTLE_DIR}/base16-encode.c
|
||||
${NETTLE_DIR}/base16-decode.c
|
||||
${NETTLE_DIR}/base16-meta.c
|
||||
${NETTLE_DIR}/base64-encode.c
|
||||
${NETTLE_DIR}/base64-decode.c
|
||||
${NETTLE_DIR}/base64-meta.c
|
||||
${NETTLE_DIR}/base64url-encode.c
|
||||
${NETTLE_DIR}/base64url-decode.c
|
||||
${NETTLE_DIR}/base64url-meta.c
|
||||
${NETTLE_DIR}/buffer.c
|
||||
${NETTLE_DIR}/buffer-init.c
|
||||
${NETTLE_DIR}/camellia-crypt-internal.c
|
||||
${NETTLE_DIR}/camellia-table.c
|
||||
${NETTLE_DIR}/camellia-absorb.c
|
||||
${NETTLE_DIR}/camellia-invert-key.c
|
||||
${NETTLE_DIR}/camellia128-set-encrypt-key.c
|
||||
${NETTLE_DIR}/camellia128-crypt.c
|
||||
${NETTLE_DIR}/camellia128-set-decrypt-key.c
|
||||
${NETTLE_DIR}/camellia128-meta.c
|
||||
${NETTLE_DIR}/camellia192-meta.c
|
||||
${NETTLE_DIR}/camellia256-set-encrypt-key.c
|
||||
${NETTLE_DIR}/camellia256-crypt.c
|
||||
${NETTLE_DIR}/camellia256-set-decrypt-key.c
|
||||
${NETTLE_DIR}/camellia256-meta.c
|
||||
${NETTLE_DIR}/cast128.c
|
||||
${NETTLE_DIR}/cast128-meta.c
|
||||
${NETTLE_DIR}/cbc.c
|
||||
${NETTLE_DIR}/cbc-aes128-encrypt.c
|
||||
${NETTLE_DIR}/cbc-aes192-encrypt.c
|
||||
${NETTLE_DIR}/cbc-aes256-encrypt.c
|
||||
${NETTLE_DIR}/ccm.c
|
||||
${NETTLE_DIR}/ccm-aes128.c
|
||||
${NETTLE_DIR}/ccm-aes192.c
|
||||
${NETTLE_DIR}/ccm-aes256.c
|
||||
${NETTLE_DIR}/cfb.c
|
||||
${NETTLE_DIR}/siv-cmac.c
|
||||
${NETTLE_DIR}/siv-cmac-aes128.c
|
||||
${NETTLE_DIR}/siv-cmac-aes256.c
|
||||
${NETTLE_DIR}/cnd-memcpy.c
|
||||
${NETTLE_DIR}/chacha-crypt.c
|
||||
${NETTLE_DIR}/chacha-core-internal.c
|
||||
${NETTLE_DIR}/chacha-poly1305.c
|
||||
${NETTLE_DIR}/chacha-poly1305-meta.c
|
||||
${NETTLE_DIR}/chacha-set-key.c
|
||||
${NETTLE_DIR}/chacha-set-nonce.c
|
||||
${NETTLE_DIR}/ctr.c
|
||||
${NETTLE_DIR}/ctr16.c
|
||||
${NETTLE_DIR}/des.c
|
||||
${NETTLE_DIR}/des3.c
|
||||
${NETTLE_DIR}/eax.c
|
||||
${NETTLE_DIR}/eax-aes128.c
|
||||
${NETTLE_DIR}/eax-aes128-meta.c
|
||||
${NETTLE_DIR}/ghash-set-key.c
|
||||
${NETTLE_DIR}/ghash-update.c
|
||||
${NETTLE_DIR}/gcm.c
|
||||
${NETTLE_DIR}/gcm-aes.c
|
||||
${NETTLE_DIR}/gcm-aes128.c
|
||||
${NETTLE_DIR}/gcm-aes128-meta.c
|
||||
${NETTLE_DIR}/gcm-aes192.c
|
||||
${NETTLE_DIR}/gcm-aes192-meta.c
|
||||
${NETTLE_DIR}/gcm-aes256.c
|
||||
${NETTLE_DIR}/gcm-aes256-meta.c
|
||||
${NETTLE_DIR}/gcm-camellia128.c
|
||||
${NETTLE_DIR}/gcm-camellia128-meta.c
|
||||
${NETTLE_DIR}/gcm-camellia256.c
|
||||
${NETTLE_DIR}/gcm-camellia256-meta.c
|
||||
${NETTLE_DIR}/cmac.c
|
||||
${NETTLE_DIR}/cmac64.c
|
||||
${NETTLE_DIR}/cmac-aes128.c
|
||||
${NETTLE_DIR}/cmac-aes256.c
|
||||
${NETTLE_DIR}/cmac-des3.c
|
||||
${NETTLE_DIR}/cmac-aes128-meta.c
|
||||
${NETTLE_DIR}/cmac-aes256-meta.c
|
||||
${NETTLE_DIR}/cmac-des3-meta.c
|
||||
${NETTLE_DIR}/gost28147.c
|
||||
${NETTLE_DIR}/gosthash94.c
|
||||
${NETTLE_DIR}/gosthash94-meta.c
|
||||
${NETTLE_DIR}/hmac.c
|
||||
${NETTLE_DIR}/hmac-gosthash94.c
|
||||
${NETTLE_DIR}/hmac-md5.c
|
||||
${NETTLE_DIR}/hmac-ripemd160.c
|
||||
${NETTLE_DIR}/hmac-sha1.c
|
||||
${NETTLE_DIR}/hmac-sha224.c
|
||||
${NETTLE_DIR}/hmac-sha256.c
|
||||
${NETTLE_DIR}/hmac-sha384.c
|
||||
${NETTLE_DIR}/hmac-sha512.c
|
||||
${NETTLE_DIR}/hmac-streebog.c
|
||||
${NETTLE_DIR}/hmac-sm3.c
|
||||
${NETTLE_DIR}/hmac-md5-meta.c
|
||||
${NETTLE_DIR}/hmac-ripemd160-meta.c
|
||||
${NETTLE_DIR}/hmac-sha1-meta.c
|
||||
${NETTLE_DIR}/hmac-sha224-meta.c
|
||||
${NETTLE_DIR}/hmac-sha256-meta.c
|
||||
${NETTLE_DIR}/hmac-sha384-meta.c
|
||||
${NETTLE_DIR}/hmac-sha512-meta.c
|
||||
${NETTLE_DIR}/hmac-streebog-meta.c
|
||||
${NETTLE_DIR}/hmac-sm3-meta.c
|
||||
${NETTLE_DIR}/knuth-lfib.c
|
||||
${NETTLE_DIR}/hkdf.c
|
||||
${NETTLE_DIR}/md2.c
|
||||
${NETTLE_DIR}/md2-meta.c
|
||||
${NETTLE_DIR}/md4.c
|
||||
${NETTLE_DIR}/md4-meta.c
|
||||
${NETTLE_DIR}/md5.c
|
||||
${NETTLE_DIR}/md5-compress.c
|
||||
${NETTLE_DIR}/md5-compat.c
|
||||
${NETTLE_DIR}/md5-meta.c
|
||||
${NETTLE_DIR}/memeql-sec.c
|
||||
${NETTLE_DIR}/memxor.c
|
||||
${NETTLE_DIR}/memxor3.c
|
||||
${NETTLE_DIR}/nettle-lookup-hash.c
|
||||
${NETTLE_DIR}/nettle-meta-aeads.c
|
||||
${NETTLE_DIR}/nettle-meta-armors.c
|
||||
${NETTLE_DIR}/nettle-meta-ciphers.c
|
||||
${NETTLE_DIR}/nettle-meta-hashes.c
|
||||
${NETTLE_DIR}/nettle-meta-macs.c
|
||||
${NETTLE_DIR}/pbkdf2.c
|
||||
${NETTLE_DIR}/pbkdf2-hmac-gosthash94.c
|
||||
${NETTLE_DIR}/pbkdf2-hmac-sha1.c
|
||||
${NETTLE_DIR}/pbkdf2-hmac-sha256.c
|
||||
${NETTLE_DIR}/pbkdf2-hmac-sha384.c
|
||||
${NETTLE_DIR}/pbkdf2-hmac-sha512.c
|
||||
${NETTLE_DIR}/poly1305-aes.c
|
||||
${NETTLE_DIR}/poly1305-internal.c
|
||||
${NETTLE_DIR}/realloc.c
|
||||
${NETTLE_DIR}/ripemd160.c
|
||||
${NETTLE_DIR}/ripemd160-compress.c
|
||||
${NETTLE_DIR}/ripemd160-meta.c
|
||||
${NETTLE_DIR}/salsa20-core-internal.c
|
||||
${NETTLE_DIR}/salsa20-crypt-internal.c
|
||||
${NETTLE_DIR}/salsa20-crypt.c
|
||||
${NETTLE_DIR}/salsa20r12-crypt.c
|
||||
${NETTLE_DIR}/salsa20-set-key.c
|
||||
${NETTLE_DIR}/salsa20-set-nonce.c
|
||||
${NETTLE_DIR}/salsa20-128-set-key.c
|
||||
${NETTLE_DIR}/salsa20-256-set-key.c
|
||||
${NETTLE_DIR}/sha1.c
|
||||
${NETTLE_DIR}/sha1-compress.c
|
||||
${NETTLE_DIR}/sha1-meta.c
|
||||
${NETTLE_DIR}/sha256.c
|
||||
${NETTLE_DIR}/sha256-compress-n.c
|
||||
${NETTLE_DIR}/sha224-meta.c
|
||||
${NETTLE_DIR}/sha256-meta.c
|
||||
${NETTLE_DIR}/sha512.c
|
||||
${NETTLE_DIR}/sha512-compress.c
|
||||
${NETTLE_DIR}/sha384-meta.c
|
||||
${NETTLE_DIR}/sha512-meta.c
|
||||
${NETTLE_DIR}/sha512-224-meta.c
|
||||
${NETTLE_DIR}/sha512-256-meta.c
|
||||
${NETTLE_DIR}/sha3.c
|
||||
${NETTLE_DIR}/sha3-permute.c
|
||||
${NETTLE_DIR}/sha3-224.c
|
||||
${NETTLE_DIR}/sha3-224-meta.c
|
||||
${NETTLE_DIR}/sha3-256.c
|
||||
${NETTLE_DIR}/sha3-256-meta.c
|
||||
${NETTLE_DIR}/sha3-384.c
|
||||
${NETTLE_DIR}/sha3-384-meta.c
|
||||
${NETTLE_DIR}/sha3-512.c
|
||||
${NETTLE_DIR}/sha3-512-meta.c
|
||||
${NETTLE_DIR}/shake256.c
|
||||
${NETTLE_DIR}/sm3.c
|
||||
${NETTLE_DIR}/sm3-meta.c
|
||||
${NETTLE_DIR}/serpent-set-key.c
|
||||
${NETTLE_DIR}/serpent-encrypt.c
|
||||
${NETTLE_DIR}/serpent-decrypt.c
|
||||
${NETTLE_DIR}/serpent-meta.c
|
||||
${NETTLE_DIR}/streebog.c
|
||||
${NETTLE_DIR}/streebog-meta.c
|
||||
${NETTLE_DIR}/twofish.c
|
||||
${NETTLE_DIR}/twofish-meta.c
|
||||
${NETTLE_DIR}/umac-nh.c
|
||||
${NETTLE_DIR}/umac-nh-n.c
|
||||
${NETTLE_DIR}/umac-l2.c
|
||||
${NETTLE_DIR}/umac-l3.c
|
||||
${NETTLE_DIR}/umac-poly64.c
|
||||
${NETTLE_DIR}/umac-poly128.c
|
||||
${NETTLE_DIR}/umac-set-key.c
|
||||
${NETTLE_DIR}/umac32.c
|
||||
${NETTLE_DIR}/umac64.c
|
||||
${NETTLE_DIR}/umac96.c
|
||||
${NETTLE_DIR}/umac128.c
|
||||
${NETTLE_DIR}/version.c
|
||||
${NETTLE_DIR}/write-be32.c
|
||||
${NETTLE_DIR}/write-le32.c
|
||||
${NETTLE_DIR}/write-le64.c
|
||||
${NETTLE_DIR}/yarrow256.c
|
||||
${NETTLE_DIR}/yarrow_key_event.c
|
||||
${NETTLE_DIR}/xts.c
|
||||
${NETTLE_DIR}/xts-aes128.c
|
||||
${NETTLE_DIR}/xts-aes256.c
|
||||
)
|
||||
|
||||
set(LIBNETTLE_INCLUDES
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
${NETTLE_DIR}
|
||||
)
|
||||
|
||||
add_library(nettle STATIC ${LIBNETTLE_SOURCES})
|
||||
target_include_directories(nettle PUBLIC ${LIBNETTLE_INCLUDES})
|
||||
target_compile_definitions(nettle PRIVATE ${LIBNETTLE_DEFINITIONS})
|
||||
@@ -1,280 +0,0 @@
|
||||
/* config.h. Generated from config.h.in by configure. */
|
||||
/* config.h.in. Generated from configure.ac by autoheader. */
|
||||
|
||||
/* Define if building universal (internal helper macro) */
|
||||
/* #undef AC_APPLE_UNIVERSAL_BUILD */
|
||||
|
||||
/* Define to one of `_getb67', `GETB67', `getb67' for Cray-2 and Cray-YMP
|
||||
systems. This function is required for `alloca.c' support on those systems.
|
||||
*/
|
||||
/* #undef CRAY_STACKSEG_END */
|
||||
|
||||
/* Define to 1 if using `alloca.c'. */
|
||||
/* #undef C_ALLOCA */
|
||||
|
||||
/* Define to 1 if you have `alloca', as a function or macro. */
|
||||
#define HAVE_ALLOCA 1
|
||||
|
||||
/* Define to 1 if you have <alloca.h> and it should be used (not on Ultrix).
|
||||
*/
|
||||
#define HAVE_ALLOCA_H 1
|
||||
|
||||
/* Define if __builtin_bswap64 is available */
|
||||
#define HAVE_BUILTIN_BSWAP64 1
|
||||
|
||||
/* Define if clock_gettime is available */
|
||||
#define HAVE_CLOCK_GETTIME 1
|
||||
|
||||
/* Define to 1 if you have the <dlfcn.h> header file. */
|
||||
#define HAVE_DLFCN_H 1
|
||||
|
||||
/* Define if fcntl file locking is available */
|
||||
#define HAVE_FCNTL_LOCKING 1
|
||||
|
||||
/* Define if the compiler understands __attribute__ */
|
||||
#define HAVE_GCC_ATTRIBUTE 1
|
||||
|
||||
/* Define to 1 if you have the `getline' function. */
|
||||
#define HAVE_GETLINE 1
|
||||
|
||||
/* Define to 1 if you have the <inttypes.h> header file. */
|
||||
#define HAVE_INTTYPES_H 1
|
||||
|
||||
/* Define to 1 if you have dlopen (with -ldl). */
|
||||
#define HAVE_LIBDL 1
|
||||
|
||||
/* Define to 1 if you have the `gmp' library (-lgmp). */
|
||||
/* #undef HAVE_LIBGMP */
|
||||
|
||||
/* Define if compiler and linker supports __attribute__ ifunc */
|
||||
#define HAVE_LINK_IFUNC 1
|
||||
|
||||
/* Define to 1 if you have the <malloc.h> header file. */
|
||||
#define HAVE_MALLOC_H 1
|
||||
|
||||
/* Define to 1 if you have the <memory.h> header file. */
|
||||
#define HAVE_MEMORY_H 1
|
||||
|
||||
/* Define to 1 each of the following for which a native (ie. CPU specific)
|
||||
implementation of the corresponding routine exists. */
|
||||
/* #undef HAVE_NATIVE_memxor3 */
|
||||
/* #undef HAVE_NATIVE_aes_decrypt */
|
||||
/* #undef HAVE_NATIVE_aes_encrypt */
|
||||
/* #undef HAVE_NATIVE_aes128_decrypt */
|
||||
/* #undef HAVE_NATIVE_aes128_encrypt */
|
||||
/* #undef HAVE_NATIVE_aes128_invert_key */
|
||||
/* #undef HAVE_NATIVE_aes128_set_decrypt_key */
|
||||
/* #undef HAVE_NATIVE_aes128_set_encrypt_key */
|
||||
/* #undef HAVE_NATIVE_aes192_decrypt */
|
||||
/* #undef HAVE_NATIVE_aes192_encrypt */
|
||||
/* #undef HAVE_NATIVE_aes192_invert_key */
|
||||
/* #undef HAVE_NATIVE_aes192_set_decrypt_key */
|
||||
/* #undef HAVE_NATIVE_aes192_set_encrypt_key */
|
||||
/* #undef HAVE_NATIVE_aes256_decrypt */
|
||||
/* #undef HAVE_NATIVE_aes256_encrypt */
|
||||
/* #undef HAVE_NATIVE_aes256_invert_key */
|
||||
/* #undef HAVE_NATIVE_aes256_set_decrypt_key */
|
||||
/* #undef HAVE_NATIVE_aes256_set_encrypt_key */
|
||||
/* #undef HAVE_NATIVE_cbc_aes128_encrypt */
|
||||
/* #undef HAVE_NATIVE_cbc_aes192_encrypt */
|
||||
/* #undef HAVE_NATIVE_cbc_aes256_encrypt */
|
||||
/* #undef HAVE_NATIVE_chacha_core */
|
||||
/* #undef HAVE_NATIVE_chacha_2core */
|
||||
#define HAVE_NATIVE_chacha_3core 1
|
||||
/* #undef HAVE_NATIVE_chacha_4core */
|
||||
/* #undef HAVE_NATIVE_fat_chacha_2core */
|
||||
/* #undef HAVE_NATIVE_fat_chacha_3core */
|
||||
/* #undef HAVE_NATIVE_fat_chacha_4core */
|
||||
/* #undef HAVE_NATIVE_ecc_curve25519_modp */
|
||||
/* #undef HAVE_NATIVE_ecc_curve448_modp */
|
||||
/* #undef HAVE_NATIVE_ecc_secp192r1_modp */
|
||||
/* #undef HAVE_NATIVE_ecc_secp192r1_redc */
|
||||
/* #undef HAVE_NATIVE_ecc_secp224r1_modp */
|
||||
/* #undef HAVE_NATIVE_ecc_secp224r1_redc */
|
||||
/* #undef HAVE_NATIVE_ecc_secp256r1_modp */
|
||||
/* #undef HAVE_NATIVE_ecc_secp256r1_redc */
|
||||
/* #undef HAVE_NATIVE_ecc_secp384r1_modp */
|
||||
/* #undef HAVE_NATIVE_ecc_secp384r1_redc */
|
||||
/* #undef HAVE_NATIVE_ecc_secp521r1_modp */
|
||||
/* #undef HAVE_NATIVE_ecc_secp521r1_redc */
|
||||
/* #undef HAVE_NATIVE_poly1305_set_key */
|
||||
/* #undef HAVE_NATIVE_poly1305_block */
|
||||
/* #undef HAVE_NATIVE_poly1305_digest */
|
||||
/* #undef HAVE_NATIVE_poly1305_blocks */
|
||||
/* #undef HAVE_NATIVE_fat_poly1305_blocks */
|
||||
/* #undef HAVE_NATIVE_ghash_set_key */
|
||||
/* #undef HAVE_NATIVE_ghash_update */
|
||||
/* #undef HAVE_NATIVE_salsa20_core */
|
||||
#define HAVE_NATIVE_salsa20_2core 1
|
||||
/* #undef HAVE_NATIVE_fat_salsa20_2core */
|
||||
/* #undef HAVE_NATIVE_sha1_compress */
|
||||
/* #undef HAVE_NATIVE_sha256_compress_n */
|
||||
/* #undef HAVE_NATIVE_sha512_compress */
|
||||
/* #undef HAVE_NATIVE_sha3_permute */
|
||||
/* #undef HAVE_NATIVE_umac_nh */
|
||||
/* #undef HAVE_NATIVE_umac_nh_n */
|
||||
|
||||
/* Define to 1 if you have the <openssl/ecdsa.h> header file. */
|
||||
/* #undef HAVE_OPENSSL_ECDSA_H */
|
||||
|
||||
/* Define to 1 if you have the <openssl/evp.h> header file. */
|
||||
/* #undef HAVE_OPENSSL_EVP_H */
|
||||
|
||||
/* Define to 1 if you have the `secure_getenv' function. */
|
||||
#define HAVE_SECURE_GETENV 1
|
||||
|
||||
/* Define to 1 if you have the <stdint.h> header file. */
|
||||
#define HAVE_STDINT_H 1
|
||||
|
||||
/* Define to 1 if you have the <stdlib.h> header file. */
|
||||
#define HAVE_STDLIB_H 1
|
||||
|
||||
/* Define to 1 if you have the `strerror' function. */
|
||||
#define HAVE_STRERROR 1
|
||||
|
||||
/* Define to 1 if you have the <strings.h> header file. */
|
||||
#define HAVE_STRINGS_H 1
|
||||
|
||||
/* Define to 1 if you have the <string.h> header file. */
|
||||
#define HAVE_STRING_H 1
|
||||
|
||||
/* Define to 1 if you have the <sys/stat.h> header file. */
|
||||
#define HAVE_SYS_STAT_H 1
|
||||
|
||||
/* Define to 1 if you have the <sys/types.h> header file. */
|
||||
#define HAVE_SYS_TYPES_H 1
|
||||
|
||||
/* Define to 1 if you have the <unistd.h> header file. */
|
||||
#define HAVE_UNISTD_H 1
|
||||
|
||||
/* Define to 1 if you have the <valgrind/memcheck.h> header file. */
|
||||
/* #undef HAVE_VALGRIND_MEMCHECK_H */
|
||||
|
||||
/* Define to the address where bug reports for this package should be sent. */
|
||||
#define PACKAGE_BUGREPORT "nettle-bugs@lists.lysator.liu.se"
|
||||
|
||||
/* Define to the full name of this package. */
|
||||
#define PACKAGE_NAME "nettle"
|
||||
|
||||
/* Define to the full name and version of this package. */
|
||||
#define PACKAGE_STRING "nettle 3.9.1"
|
||||
|
||||
/* Define to the one symbol short name of this package. */
|
||||
#define PACKAGE_TARNAME "nettle"
|
||||
|
||||
/* Define to the home page for this package. */
|
||||
#define PACKAGE_URL ""
|
||||
|
||||
/* Define to the version of this package. */
|
||||
#define PACKAGE_VERSION "3.9.1"
|
||||
|
||||
/* The size of `long', as computed by sizeof. */
|
||||
#define SIZEOF_LONG __SIZEOF_LONG__
|
||||
|
||||
/* The size of `size_t', as computed by sizeof. */
|
||||
#define SIZEOF_SIZE_T __SIZEOF_SIZE_T__
|
||||
|
||||
/* If using the C implementation of alloca, define if you know the
|
||||
direction of stack growth for your system; otherwise it will be
|
||||
automatically deduced at runtime.
|
||||
STACK_DIRECTION > 0 => grows toward higher addresses
|
||||
STACK_DIRECTION < 0 => grows toward lower addresses
|
||||
STACK_DIRECTION = 0 => direction of growth unknown */
|
||||
/* #undef STACK_DIRECTION */
|
||||
|
||||
/* Define to 1 if you have the ANSI C header files. */
|
||||
#define STDC_HEADERS 1
|
||||
|
||||
/* Define to 1 if you can safely include both <sys/time.h> and <time.h>. */
|
||||
#define TIME_WITH_SYS_TIME 1
|
||||
|
||||
/* Defined if public key features are enabled */
|
||||
/* #undef WITH_HOGWEED */
|
||||
|
||||
/* Define if you have openssl's libcrypto (used for benchmarking) */
|
||||
/* #undef WITH_OPENSSL */
|
||||
|
||||
/* Define WORDS_BIGENDIAN to 1 if your processor stores words with the most
|
||||
significant byte first (like Motorola and SPARC, unlike Intel). */
|
||||
#if defined AC_APPLE_UNIVERSAL_BUILD
|
||||
# if defined __BIG_ENDIAN__
|
||||
# define WORDS_BIGENDIAN 1
|
||||
# endif
|
||||
#else
|
||||
# ifndef WORDS_BIGENDIAN
|
||||
/* # undef WORDS_BIGENDIAN */
|
||||
# endif
|
||||
#endif
|
||||
|
||||
/* Define to empty if `const' does not conform to ANSI C. */
|
||||
/* #undef const */
|
||||
|
||||
/* Define to `int' if <sys/types.h> doesn't define. */
|
||||
/* #undef gid_t */
|
||||
|
||||
/* Define to `__inline__' or `__inline' if that's what the C compiler
|
||||
calls it, or to nothing if 'inline' is not supported under any name. */
|
||||
#ifndef __cplusplus
|
||||
/* #undef inline */
|
||||
#endif
|
||||
|
||||
/* Define to `unsigned int' if <sys/types.h> does not define. */
|
||||
/* #undef size_t */
|
||||
|
||||
/* Define to `int' if <sys/types.h> doesn't define. */
|
||||
/* #undef uid_t */
|
||||
|
||||
/* AIX requires this to be the first thing in the file. */
|
||||
#ifndef __GNUC__
|
||||
# if HAVE_ALLOCA_H
|
||||
# include <alloca.h>
|
||||
# else
|
||||
# ifdef _AIX
|
||||
#pragma alloca
|
||||
# else
|
||||
# ifndef alloca /* predefined by HP cc +Olibcalls */
|
||||
char *alloca ();
|
||||
# endif
|
||||
# endif
|
||||
/* Needed for alloca on windows */
|
||||
# if HAVE_MALLOC_H
|
||||
# include <malloc.h>
|
||||
# endif
|
||||
# endif
|
||||
#else /* defined __GNUC__ */
|
||||
# if HAVE_ALLOCA_H
|
||||
# include <alloca.h>
|
||||
# else
|
||||
/* Needed for alloca on windows, also with gcc */
|
||||
# if HAVE_MALLOC_H
|
||||
# include <malloc.h>
|
||||
# endif
|
||||
# endif
|
||||
#endif
|
||||
|
||||
|
||||
#if HAVE_STRERROR
|
||||
#define STRERROR strerror
|
||||
#else
|
||||
#define STRERROR(x) (sys_errlist[x])
|
||||
#endif
|
||||
|
||||
|
||||
#if __GNUC__ && HAVE_GCC_ATTRIBUTE
|
||||
# define NORETURN __attribute__ ((__noreturn__))
|
||||
# define PRINTF_STYLE(f, a) __attribute__ ((__format__ (__printf__, f, a)))
|
||||
# define UNUSED __attribute__ ((__unused__))
|
||||
#else
|
||||
# define NORETURN
|
||||
# define PRINTF_STYLE(f, a)
|
||||
# define UNUSED
|
||||
#endif
|
||||
|
||||
|
||||
#if defined(__x86_64__) || defined(__arch64__)
|
||||
# define HAVE_NATIVE_64_BIT 1
|
||||
#else
|
||||
/* Needs include of <limits.h> before use. */
|
||||
# define HAVE_NATIVE_64_BIT (SIZEOF_LONG * CHAR_BIT >= 64)
|
||||
#endif
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
/* version.h
|
||||
|
||||
Information about library version.
|
||||
|
||||
Copyright (C) 2015 Red Hat, Inc.
|
||||
Copyright (C) 2015 Niels Möller
|
||||
|
||||
This file is part of GNU Nettle.
|
||||
|
||||
GNU Nettle is free software: you can redistribute it and/or
|
||||
modify it under the terms of either:
|
||||
|
||||
* the GNU Lesser General Public License as published by the Free
|
||||
Software Foundation; either version 3 of the License, or (at your
|
||||
option) any later version.
|
||||
|
||||
or
|
||||
|
||||
* the GNU General Public License as published by the Free
|
||||
Software Foundation; either version 2 of the License, or (at your
|
||||
option) any later version.
|
||||
|
||||
or both in parallel, as here.
|
||||
|
||||
GNU Nettle is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
General Public License for more details.
|
||||
|
||||
You should have received copies of the GNU General Public License and
|
||||
the GNU Lesser General Public License along with this program. If
|
||||
not, see http://www.gnu.org/licenses/.
|
||||
*/
|
||||
|
||||
#ifndef NETTLE_VERSION_H_INCLUDED
|
||||
#define NETTLE_VERSION_H_INCLUDED
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* Individual version numbers in decimal */
|
||||
#define NETTLE_VERSION_MAJOR 3
|
||||
#define NETTLE_VERSION_MINOR 9
|
||||
|
||||
#define NETTLE_USE_MINI_GMP 0
|
||||
|
||||
/* We need a preprocessor constant for GMP_NUMB_BITS, simply using
|
||||
sizeof(mp_limb_t) * CHAR_BIT is not good enough. */
|
||||
#if NETTLE_USE_MINI_GMP
|
||||
# define GMP_NUMB_BITS n/a
|
||||
#endif
|
||||
|
||||
int
|
||||
nettle_version_major (void);
|
||||
|
||||
int
|
||||
nettle_version_minor (void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* NETTLE_VERSION_H_INCLUDED */
|
||||
+460
@@ -0,0 +1,460 @@
|
||||
/*
|
||||
* Copyright 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package androidx.compose.foundation.gestures
|
||||
|
||||
import androidx.compose.animation.core.AnimationSpec
|
||||
import androidx.compose.animation.core.DecayAnimationSpec
|
||||
import androidx.compose.animation.core.FloatDecayAnimationSpec
|
||||
import androidx.compose.animation.core.generateDecayAnimationSpec
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.gestures.HorizontalDragEvent.DragDelta
|
||||
import androidx.compose.foundation.gestures.snapping.SnapLayoutInfoProvider
|
||||
import androidx.compose.foundation.gestures.snapping.snapFlingBehavior
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.input.pointer.PointerInputChange
|
||||
import androidx.compose.ui.input.pointer.positionChange
|
||||
import androidx.compose.ui.layout.Measurable
|
||||
import androidx.compose.ui.layout.MeasureResult
|
||||
import androidx.compose.ui.layout.MeasureScope
|
||||
import androidx.compose.ui.node.LayoutModifierNode
|
||||
import androidx.compose.ui.node.ModifierNodeElement
|
||||
import androidx.compose.ui.node.ObserverModifierNode
|
||||
import androidx.compose.ui.node.currentValueOf
|
||||
import androidx.compose.ui.node.observeReads
|
||||
import androidx.compose.ui.node.requireLayoutDirection
|
||||
import androidx.compose.ui.platform.InspectorInfo
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.debugInspectorInfo
|
||||
import androidx.compose.ui.unit.Constraints
|
||||
import androidx.compose.ui.unit.Density
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import androidx.compose.ui.unit.Velocity
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.absoluteValue
|
||||
import kotlin.math.roundToInt
|
||||
import kotlin.math.sign
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@ExperimentalFoundationApi
|
||||
fun <T> Modifier.anchoredHorizontalDraggable(
|
||||
state: AnchoredDraggableState<T>,
|
||||
enableDragFromStartToEnd: Boolean = true,
|
||||
enableDragFromEndToStart: Boolean = true,
|
||||
enabled: Boolean = true,
|
||||
startDragImmediately: Boolean = state.isAnimationRunning,
|
||||
flingBehavior: FlingBehavior? = null,
|
||||
): Modifier = this then AnchoredHorizontalDraggableElement(
|
||||
state = state,
|
||||
canDrag = { (x, y) ->
|
||||
val enableDrag = if (x > 0) enableDragFromStartToEnd else enableDragFromEndToStart
|
||||
enableDrag && x.absoluteValue > y.absoluteValue * 2f
|
||||
},
|
||||
enabled = enabled,
|
||||
startDragImmediately = startDragImmediately,
|
||||
flingBehavior = flingBehavior,
|
||||
)
|
||||
|
||||
private class AnchoredHorizontalDraggableElement<T>(
|
||||
private val state: AnchoredDraggableState<T>,
|
||||
private val canDrag: (Offset) -> Boolean,
|
||||
private val enabled: Boolean,
|
||||
private val startDragImmediately: Boolean,
|
||||
private val flingBehavior: FlingBehavior?,
|
||||
) : ModifierNodeElement<AnchoredHorizontalDraggableNode<T>>() {
|
||||
override fun create() = AnchoredHorizontalDraggableNode(
|
||||
state,
|
||||
canDrag,
|
||||
enabled,
|
||||
startDragImmediately,
|
||||
flingBehavior,
|
||||
)
|
||||
|
||||
override fun update(node: AnchoredHorizontalDraggableNode<T>) {
|
||||
node.update(
|
||||
state,
|
||||
canDrag,
|
||||
enabled,
|
||||
startDragImmediately,
|
||||
flingBehavior,
|
||||
)
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = state.hashCode()
|
||||
result = 31 * result + canDrag.hashCode()
|
||||
result = 31 * result + enabled.hashCode()
|
||||
result = 31 * result + startDragImmediately.hashCode()
|
||||
result = 31 * result + flingBehavior.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
|
||||
if (other !is AnchoredHorizontalDraggableElement<*>) return false
|
||||
|
||||
if (state != other.state) return false
|
||||
if (canDrag != other.canDrag) return false
|
||||
if (enabled != other.enabled) return false
|
||||
if (startDragImmediately != other.startDragImmediately) return false
|
||||
if (flingBehavior != other.flingBehavior) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun InspectorInfo.inspectableProperties() {
|
||||
name = "anchoredDraggable"
|
||||
properties["state"] = state
|
||||
properties["canDrag"] = canDrag
|
||||
properties["enabled"] = enabled
|
||||
properties["startDragImmediately"] = startDragImmediately
|
||||
properties["flingBehavior"] = flingBehavior
|
||||
}
|
||||
}
|
||||
|
||||
private class AnchoredHorizontalDraggableNode<T>(
|
||||
private var state: AnchoredDraggableState<T>,
|
||||
private var canDrag: (Offset) -> Boolean,
|
||||
enabled: Boolean,
|
||||
private var startDragImmediately: Boolean,
|
||||
private var flingBehavior: FlingBehavior?,
|
||||
) : HorizontalDragGestureNode(enabled = enabled),
|
||||
ObserverModifierNode {
|
||||
|
||||
lateinit var resolvedFlingBehavior: FlingBehavior
|
||||
lateinit var density: Density
|
||||
|
||||
private val isReverseDirection: Boolean
|
||||
get() = requireLayoutDirection() == LayoutDirection.Rtl
|
||||
|
||||
override fun onAttach() {
|
||||
updateFlingBehavior(flingBehavior)
|
||||
}
|
||||
|
||||
override fun onObservedReadsChanged() {
|
||||
val newDensity = currentValueOf(LocalDensity)
|
||||
if (density != newDensity) {
|
||||
density = newDensity
|
||||
updateFlingBehavior(flingBehavior)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateFlingBehavior(newFlingBehavior: FlingBehavior?) {
|
||||
// Fall back to default fling behavior if the new fling behavior is null
|
||||
this.resolvedFlingBehavior =
|
||||
if (newFlingBehavior == null) {
|
||||
// Only register for LocalDensity snapshot updates if we are creating a decay
|
||||
observeReads { density = currentValueOf(LocalDensity) }
|
||||
anchoredDraggableFlingBehavior(
|
||||
snapAnimationSpec = AnchoredDraggableDefaults.SnapAnimationSpec,
|
||||
positionalThreshold = AnchoredDraggableDefaults.PositionalThreshold,
|
||||
density = density,
|
||||
state = state,
|
||||
)
|
||||
} else {
|
||||
newFlingBehavior
|
||||
}
|
||||
}
|
||||
|
||||
override fun canDrag(change: PointerInputChange): Boolean = canDrag(change.positionChange().reverseIfNeeded())
|
||||
|
||||
override suspend fun drag(forEachDelta: suspend ((dragDelta: DragDelta) -> Unit) -> Unit) {
|
||||
state.anchoredDrag {
|
||||
forEachDelta { dragDelta ->
|
||||
dragTo(state.newOffsetForDelta(dragDelta.delta.reverseIfNeeded().x))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDragStarted(startedPosition: Offset) { }
|
||||
|
||||
override fun onDragStopped(velocity: Velocity) {
|
||||
if (!isAttached) return
|
||||
coroutineScope.launch {
|
||||
fling(velocity.reverseIfNeeded().x)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun fling(velocity: Float): Float {
|
||||
var leftoverVelocity = velocity
|
||||
state.anchoredDrag {
|
||||
val scrollScope =
|
||||
object : ScrollScope {
|
||||
override fun scrollBy(pixels: Float): Float {
|
||||
val newOffset = state.newOffsetForDelta(pixels)
|
||||
val consumed = newOffset - state.offset
|
||||
dragTo(newOffset)
|
||||
return consumed
|
||||
}
|
||||
}
|
||||
with(resolvedFlingBehavior) {
|
||||
leftoverVelocity = scrollScope.performFling(velocity)
|
||||
}
|
||||
}
|
||||
return leftoverVelocity
|
||||
}
|
||||
|
||||
override fun startDragImmediately(): Boolean = startDragImmediately
|
||||
|
||||
fun update(
|
||||
state: AnchoredDraggableState<T>,
|
||||
canDrag: (Offset) -> Boolean,
|
||||
enabled: Boolean,
|
||||
startDragImmediately: Boolean,
|
||||
flingBehavior: FlingBehavior?,
|
||||
) {
|
||||
this.flingBehavior = flingBehavior
|
||||
|
||||
var resetPointerInputHandling = false
|
||||
|
||||
if (this.state != state) {
|
||||
this.state = state
|
||||
updateFlingBehavior(flingBehavior)
|
||||
resetPointerInputHandling = true
|
||||
}
|
||||
|
||||
this.canDrag = canDrag
|
||||
this.startDragImmediately = startDragImmediately
|
||||
|
||||
update(
|
||||
enabled = enabled,
|
||||
shouldResetPointerInputHandling = resetPointerInputHandling,
|
||||
)
|
||||
}
|
||||
|
||||
private fun Velocity.reverseIfNeeded() = if (isReverseDirection) this * -1f else this * 1f
|
||||
private fun Offset.reverseIfNeeded() = if (isReverseDirection) this * -1f else this * 1f
|
||||
}
|
||||
|
||||
internal fun <T> AnchoredDraggableState<T>.newOffsetForDelta(delta: Float) =
|
||||
((if (offset.isNaN()) 0f else offset) + delta)
|
||||
.coerceIn(anchors.minPosition(), anchors.maxPosition())
|
||||
|
||||
/**
|
||||
* Compute the target anchor based on the [currentOffset], [velocity] and [positionalThreshold] and
|
||||
* [velocityThreshold].
|
||||
*
|
||||
* @return The suggested target anchor
|
||||
*/
|
||||
@ExperimentalFoundationApi
|
||||
private fun <T> DraggableAnchors<T>.computeTarget(
|
||||
currentOffset: Float,
|
||||
currentValue: T,
|
||||
velocity: Float,
|
||||
positionalThreshold: (totalDistance: Float) -> Float,
|
||||
velocityThreshold: () -> Float,
|
||||
): T {
|
||||
val currentAnchors = this
|
||||
val currentAnchorPosition = currentAnchors.positionOf(currentValue)
|
||||
val velocityThresholdPx = velocityThreshold()
|
||||
return if (currentAnchorPosition == currentOffset || currentAnchorPosition.isNaN()) {
|
||||
currentValue
|
||||
} else {
|
||||
if (abs(velocity) >= abs(velocityThresholdPx)) {
|
||||
currentAnchors.closestAnchor(currentOffset, sign(velocity) > 0)!!
|
||||
} else {
|
||||
val neighborAnchor =
|
||||
currentAnchors.closestAnchor(
|
||||
currentOffset,
|
||||
currentOffset - currentAnchorPosition > 0,
|
||||
)!!
|
||||
val neighborAnchorPosition = currentAnchors.positionOf(neighborAnchor)
|
||||
val distance = abs(currentAnchorPosition - neighborAnchorPosition)
|
||||
val relativeThreshold = abs(positionalThreshold(distance))
|
||||
val relativePosition = abs(currentAnchorPosition - currentOffset)
|
||||
if (relativePosition <= relativeThreshold) currentValue else neighborAnchor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a [FlingBehavior] for use with [Modifier.anchoredDraggable].
|
||||
*
|
||||
* @param state The [AnchoredDraggableState] that will be used for the fling animation
|
||||
* @param positionalThreshold A positional threshold that needs to be crossed in order to reach the
|
||||
* next anchor when flinging, in pixels. This can be a derived from the distance that the lambda
|
||||
* is invoked with.
|
||||
* @param snapAnimationSpec The animation spec that will be used to snap to a new state.
|
||||
*/
|
||||
@ExperimentalFoundationApi
|
||||
internal fun <T> anchoredDraggableFlingBehavior(
|
||||
state: AnchoredDraggableState<T>,
|
||||
density: Density,
|
||||
positionalThreshold: (totalDistance: Float) -> Float,
|
||||
snapAnimationSpec: AnimationSpec<Float>,
|
||||
): TargetedFlingBehavior =
|
||||
snapFlingBehavior(
|
||||
decayAnimationSpec = NoOpDecayAnimationSpec,
|
||||
snapAnimationSpec = snapAnimationSpec,
|
||||
snapLayoutInfoProvider =
|
||||
AnchoredDraggableLayoutInfoProvider(
|
||||
state = state,
|
||||
positionalThreshold = positionalThreshold,
|
||||
velocityThreshold = { with(density) { 125.dp.toPx() } },
|
||||
),
|
||||
)
|
||||
|
||||
@Suppress("FunctionName")
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
private fun <T> AnchoredDraggableLayoutInfoProvider(
|
||||
state: AnchoredDraggableState<T>,
|
||||
positionalThreshold: (totalDistance: Float) -> Float,
|
||||
velocityThreshold: () -> Float,
|
||||
): SnapLayoutInfoProvider =
|
||||
object : SnapLayoutInfoProvider {
|
||||
|
||||
// We never decay in AnchoredDraggable's fling
|
||||
override fun calculateApproachOffset(velocity: Float, decayOffset: Float) = 0f
|
||||
|
||||
override fun calculateSnapOffset(velocity: Float): Float {
|
||||
val currentOffset = state.requireOffset()
|
||||
val target =
|
||||
state.anchors.computeTarget(
|
||||
currentOffset = currentOffset,
|
||||
currentValue = state.currentValue,
|
||||
velocity = velocity,
|
||||
positionalThreshold = positionalThreshold,
|
||||
velocityThreshold = velocityThreshold,
|
||||
)
|
||||
return state.anchors.positionOf(target) - currentOffset
|
||||
}
|
||||
}
|
||||
|
||||
private val NoOpDecayAnimationSpec: DecayAnimationSpec<Float> =
|
||||
object : FloatDecayAnimationSpec {
|
||||
override val absVelocityThreshold = 0f
|
||||
|
||||
override fun getValueFromNanos(
|
||||
playTimeNanos: Long,
|
||||
initialValue: Float,
|
||||
initialVelocity: Float,
|
||||
) = 0f
|
||||
|
||||
override fun getDurationNanos(initialValue: Float, initialVelocity: Float) = 0L
|
||||
|
||||
override fun getVelocityFromNanos(
|
||||
playTimeNanos: Long,
|
||||
initialValue: Float,
|
||||
initialVelocity: Float,
|
||||
) = 0f
|
||||
|
||||
override fun getTargetValue(initialValue: Float, initialVelocity: Float) = 0f
|
||||
}.generateDecayAnimationSpec()
|
||||
|
||||
/**
|
||||
* This Modifier allows configuring an [AnchoredDraggableState]'s anchors based on this layout
|
||||
* node's size and offsetting it.
|
||||
* It considers lookahead and reports the appropriate size and measurement for the appropriate
|
||||
* phase.
|
||||
*
|
||||
* @param state The state the anchors should be attached to
|
||||
* @param orientation The orientation the component should be offset in
|
||||
* @param anchors Lambda to calculate the anchors based on this layout's size and the incoming
|
||||
* constraints. These can be useful to avoid subcomposition.
|
||||
*/
|
||||
internal fun <T> Modifier.draggableAnchors(
|
||||
state: AnchoredDraggableState<T>,
|
||||
orientation: Orientation,
|
||||
anchors: (size: IntSize, constraints: Constraints) -> Pair<DraggableAnchors<T>, T>,
|
||||
) = this then DraggableAnchorsElement(state, anchors, orientation)
|
||||
|
||||
private class DraggableAnchorsElement<T>(
|
||||
private val state: AnchoredDraggableState<T>,
|
||||
private val anchors: (size: IntSize, constraints: Constraints) -> Pair<DraggableAnchors<T>, T>,
|
||||
private val orientation: Orientation,
|
||||
) : ModifierNodeElement<DraggableAnchorsNode<T>>() {
|
||||
|
||||
override fun create() = DraggableAnchorsNode(state, anchors, orientation)
|
||||
|
||||
override fun update(node: DraggableAnchorsNode<T>) {
|
||||
node.state = state
|
||||
node.anchors = anchors
|
||||
node.orientation = orientation
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
|
||||
if (other !is DraggableAnchorsElement<*>) return false
|
||||
|
||||
if (state != other.state) return false
|
||||
if (anchors !== other.anchors) return false
|
||||
if (orientation != other.orientation) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = state.hashCode()
|
||||
result = 31 * result + anchors.hashCode()
|
||||
result = 31 * result + orientation.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
override fun InspectorInfo.inspectableProperties() {
|
||||
debugInspectorInfo {
|
||||
properties["state"] = state
|
||||
properties["anchors"] = anchors
|
||||
properties["orientation"] = orientation
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class DraggableAnchorsNode<T>(
|
||||
var state: AnchoredDraggableState<T>,
|
||||
var anchors: (size: IntSize, constraints: Constraints) -> Pair<DraggableAnchors<T>, T>,
|
||||
var orientation: Orientation,
|
||||
) : Modifier.Node(),
|
||||
LayoutModifierNode {
|
||||
private var didLookahead: Boolean = false
|
||||
|
||||
override fun onDetach() {
|
||||
didLookahead = false
|
||||
}
|
||||
|
||||
override fun MeasureScope.measure(
|
||||
measurable: Measurable,
|
||||
constraints: Constraints,
|
||||
): MeasureResult {
|
||||
val placeable = measurable.measure(constraints)
|
||||
// If we are in a lookahead pass, we only want to update the anchors here and not in
|
||||
// post-lookahead. If there is no lookahead happening (!isLookingAhead && !didLookahead),
|
||||
// update the anchors in the main pass.
|
||||
if (!isLookingAhead || !didLookahead) {
|
||||
val size = IntSize(placeable.width, placeable.height)
|
||||
val newAnchorResult = anchors(size, constraints)
|
||||
state.updateAnchors(newAnchorResult.first, newAnchorResult.second)
|
||||
}
|
||||
didLookahead = isLookingAhead || didLookahead
|
||||
return layout(placeable.width, placeable.height) {
|
||||
// In a lookahead pass, we use the position of the current target as this is where any
|
||||
// ongoing animations would move. If the component is in a settled state, lookahead
|
||||
// and post-lookahead will converge.
|
||||
val offset = if (isLookingAhead) {
|
||||
state.anchors.positionOf(state.targetValue)
|
||||
} else {
|
||||
state.requireOffset()
|
||||
}
|
||||
val xOffset = if (orientation == Orientation.Horizontal) offset else 0f
|
||||
val yOffset = if (orientation == Orientation.Vertical) offset else 0f
|
||||
placeable.placeRelative(xOffset.roundToInt(), yOffset.roundToInt())
|
||||
}
|
||||
}
|
||||
}
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
* Copyright 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package androidx.compose.foundation.gestures
|
||||
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.input.pointer.PointerEventPass
|
||||
import androidx.compose.ui.input.pointer.PointerInputChange
|
||||
import androidx.compose.ui.input.pointer.PointerInputScope
|
||||
import androidx.compose.ui.input.pointer.positionChange
|
||||
import androidx.compose.ui.platform.ViewConfiguration
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
|
||||
/**
|
||||
* A Gesture detector that waits for pointer down and touch slop in the horizontal direction and
|
||||
* then calls [onDrag] for each drag event.
|
||||
* It follows the touch slop detection of [awaitHorizontalTouchSlopOrCancellation] but will consume
|
||||
* the position change automatically once the touch slop has been crossed, the amount of drag over
|
||||
* the touch slop is reported as the first drag event [onDrag] after the slop is crossed.
|
||||
* If [shouldAwaitTouchSlop] returns true the touch slop recognition phase will be ignored
|
||||
* and the drag gesture will be recognized immediately.The first [onDrag] in this case will report
|
||||
* an [Offset.Zero].
|
||||
*
|
||||
* [onDragStart] is called when the touch slop has been passed and includes an [Offset] representing
|
||||
* the last known pointer position relative to the containing element as well as the initial
|
||||
* down event that triggered this gesture detection cycle. The [Offset] can be outside
|
||||
* the actual bounds of the element itself meaning the numbers can be negative or larger than the
|
||||
* element bounds if the touch target is smaller than the
|
||||
* [ViewConfiguration.minimumTouchTargetSize].
|
||||
*
|
||||
* [onDragEnd] is called after all pointers are up with the event change of the up event
|
||||
* and [onDragCancel] is called if another gesture has consumed pointer input,
|
||||
* canceling this gesture.
|
||||
*
|
||||
* @param onDragStart A lambda to be called when the drag gesture starts, it contains information
|
||||
* about the last known [PointerInputChange] relative to the containing element and the post slop
|
||||
* delta.
|
||||
* @param onDragEnd A lambda to be called when the gesture ends. It contains information about the
|
||||
* up [PointerInputChange] that finished the gesture.
|
||||
* @param onDragCancel A lambda to be called when the gesture is cancelled either by an error or
|
||||
* when it was consumed.
|
||||
* @param shouldAwaitTouchSlop Indicates if touch slop detection should be skipped.
|
||||
* @param onDrag A lambda to be called for each delta event in the gesture. It contains information
|
||||
* about the [PointerInputChange] and the movement offset.
|
||||
*/
|
||||
internal suspend fun PointerInputScope.detectHorizontalDragGestures(
|
||||
canDrag: (PointerInputChange) -> Boolean,
|
||||
onDragStart: (change: PointerInputChange, initialDelta: Offset) -> Unit,
|
||||
onDragEnd: () -> Unit,
|
||||
onDragCancel: () -> Unit,
|
||||
shouldAwaitTouchSlop: () -> Boolean,
|
||||
onDrag: (change: PointerInputChange, dragAmount: Offset) -> Unit,
|
||||
) {
|
||||
awaitEachGesture {
|
||||
val initialDown =
|
||||
awaitFirstDown(requireUnconsumed = false, pass = PointerEventPass.Initial)
|
||||
val awaitTouchSlop = shouldAwaitTouchSlop()
|
||||
|
||||
if (!awaitTouchSlop) {
|
||||
initialDown.consume()
|
||||
}
|
||||
val down = awaitFirstDown(requireUnconsumed = false)
|
||||
var drag: PointerInputChange?
|
||||
var overSlop = Offset.Zero
|
||||
|
||||
if (awaitTouchSlop) {
|
||||
do {
|
||||
drag = awaitHorizontalTouchSlopOrCancellation(
|
||||
down.id,
|
||||
) { change, over ->
|
||||
if (canDrag.invoke(change)) {
|
||||
change.consume()
|
||||
overSlop = Offset(over, 0f)
|
||||
} else {
|
||||
throw CancellationException()
|
||||
}
|
||||
}
|
||||
} while (drag != null && !drag.isConsumed)
|
||||
} else {
|
||||
drag = initialDown
|
||||
}
|
||||
|
||||
if (drag != null) {
|
||||
onDragStart.invoke(drag, overSlop)
|
||||
onDrag(drag, overSlop)
|
||||
val dragEnd = horizontalDrag(
|
||||
pointerId = drag.id,
|
||||
onDrag = {
|
||||
onDrag(it, it.positionChange())
|
||||
it.consume()
|
||||
},
|
||||
)
|
||||
if (dragEnd) {
|
||||
onDragEnd()
|
||||
} else {
|
||||
onDragCancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
/*
|
||||
* Copyright 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package androidx.compose.foundation.gestures
|
||||
|
||||
import androidx.compose.foundation.gestures.HorizontalDragEvent.DragCancelled
|
||||
import androidx.compose.foundation.gestures.HorizontalDragEvent.DragDelta
|
||||
import androidx.compose.foundation.gestures.HorizontalDragEvent.DragStarted
|
||||
import androidx.compose.foundation.gestures.HorizontalDragEvent.DragStopped
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.input.pointer.PointerEvent
|
||||
import androidx.compose.ui.input.pointer.PointerEventPass
|
||||
import androidx.compose.ui.input.pointer.PointerInputChange
|
||||
import androidx.compose.ui.input.pointer.SuspendingPointerInputModifierNode
|
||||
import androidx.compose.ui.input.pointer.util.VelocityTracker
|
||||
import androidx.compose.ui.input.pointer.util.addPointerInputChange
|
||||
import androidx.compose.ui.node.CompositionLocalConsumerModifierNode
|
||||
import androidx.compose.ui.node.DelegatingNode
|
||||
import androidx.compose.ui.node.PointerInputModifierNode
|
||||
import androidx.compose.ui.node.currentValueOf
|
||||
import androidx.compose.ui.platform.LocalViewConfiguration
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import androidx.compose.ui.unit.Velocity
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
import kotlin.math.sign
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* A node that performs drag gesture recognition and event propagation.
|
||||
*/
|
||||
internal abstract class HorizontalDragGestureNode(
|
||||
enabled: Boolean,
|
||||
) : DelegatingNode(),
|
||||
PointerInputModifierNode,
|
||||
CompositionLocalConsumerModifierNode {
|
||||
|
||||
protected var enabled = enabled
|
||||
private set
|
||||
|
||||
private var channel: Channel<HorizontalDragEvent>? = null
|
||||
private var isListeningForEvents = false
|
||||
|
||||
abstract fun canDrag(change: PointerInputChange): Boolean
|
||||
|
||||
/**
|
||||
* Responsible for the dragging behavior between the start and the end of the drag. It
|
||||
* continually invokes `forEachDelta` to process incoming events. In return, `forEachDelta`
|
||||
* calls `dragBy` method to process each individual delta.
|
||||
*/
|
||||
abstract suspend fun drag(forEachDelta: suspend ((dragDelta: DragDelta) -> Unit) -> Unit)
|
||||
|
||||
/**
|
||||
* Passes the action needed when a drag starts. This gives the ability to pass the desired
|
||||
* behavior from other nodes implementing AbstractDraggableNode
|
||||
*/
|
||||
abstract fun onDragStarted(startedPosition: Offset)
|
||||
|
||||
/**
|
||||
* Passes the action needed when a drag stops. This gives the ability to pass the desired
|
||||
* behavior from other nodes implementing AbstractDraggableNode
|
||||
*/
|
||||
abstract fun onDragStopped(velocity: Velocity)
|
||||
|
||||
/**
|
||||
* If touch slop recognition should be skipped. If this is true, this node will start
|
||||
* recognizing drag events immediately without waiting for touch slop.
|
||||
*/
|
||||
abstract fun startDragImmediately(): Boolean
|
||||
|
||||
private fun startListeningForEvents() {
|
||||
isListeningForEvents = true
|
||||
|
||||
/**
|
||||
* To preserve the original behavior we had (before the Modifier.Node migration) we need to
|
||||
* scope the DragStopped and DragCancel methods to the node's coroutine scope instead of using
|
||||
* the one provided by the pointer input modifier, this is to ensure that even when the pointer
|
||||
* input scope is reset we will continue any coroutine scope scope that we started from these
|
||||
* methods while the pointer input scope was active.
|
||||
*/
|
||||
coroutineScope.launch {
|
||||
while (isActive) {
|
||||
var event = channel?.receive()
|
||||
if (event !is DragStarted) continue
|
||||
onDragStarted(event.startPoint)
|
||||
try {
|
||||
drag { processDelta ->
|
||||
while (event !is DragStopped && event !is DragCancelled) {
|
||||
(event as? DragDelta)?.let(processDelta)
|
||||
event = channel?.receive()
|
||||
}
|
||||
}
|
||||
if (event is DragStopped) {
|
||||
onDragStopped((event as DragStopped).velocity)
|
||||
} else if (event is DragCancelled) {
|
||||
onDragStopped(Velocity.Zero)
|
||||
}
|
||||
} catch (c: CancellationException) {
|
||||
onDragStopped(Velocity.Zero)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var pointerInputNode: SuspendingPointerInputModifierNode? = null
|
||||
|
||||
override fun onDetach() {
|
||||
isListeningForEvents = false
|
||||
}
|
||||
|
||||
override fun onPointerEvent(
|
||||
pointerEvent: PointerEvent,
|
||||
pass: PointerEventPass,
|
||||
bounds: IntSize,
|
||||
) {
|
||||
if (enabled && pointerInputNode == null) {
|
||||
pointerInputNode = delegate(initializePointerInputNode())
|
||||
}
|
||||
pointerInputNode?.onPointerEvent(pointerEvent, pass, bounds)
|
||||
}
|
||||
|
||||
private fun initializePointerInputNode(): SuspendingPointerInputModifierNode = SuspendingPointerInputModifierNode {
|
||||
// re-create tracker when pointer input block restarts. This lazily creates the tracker
|
||||
// only when it is need.
|
||||
val velocityTracker = VelocityTracker()
|
||||
val onDragStart: (change: PointerInputChange, initialDelta: Offset) -> Unit =
|
||||
{ startEvent, initialDelta ->
|
||||
if (!isListeningForEvents) {
|
||||
if (channel == null) {
|
||||
channel = Channel(capacity = Channel.UNLIMITED)
|
||||
}
|
||||
startListeningForEvents()
|
||||
}
|
||||
val xSign = sign(startEvent.position.x)
|
||||
val ySign = sign(startEvent.position.y)
|
||||
val adjustedStart = startEvent.position -
|
||||
Offset(initialDelta.x * xSign, initialDelta.y * ySign)
|
||||
|
||||
channel?.trySend(DragStarted(adjustedStart))
|
||||
}
|
||||
|
||||
val onDragEnd: () -> Unit = {
|
||||
val maximumVelocity = currentValueOf(LocalViewConfiguration)
|
||||
.maximumFlingVelocity
|
||||
val velocity = velocityTracker.calculateVelocity(
|
||||
Velocity(maximumVelocity, maximumVelocity),
|
||||
)
|
||||
velocityTracker.resetTracking()
|
||||
channel?.trySend(DragStopped(velocity))
|
||||
}
|
||||
|
||||
val onDragCancel: () -> Unit = {
|
||||
channel?.trySend(DragCancelled)
|
||||
}
|
||||
|
||||
val shouldAwaitTouchSlop: () -> Boolean = {
|
||||
!startDragImmediately()
|
||||
}
|
||||
|
||||
val onDrag: (change: PointerInputChange, dragAmount: Offset) -> Unit =
|
||||
{ change, delta ->
|
||||
velocityTracker.addPointerInputChange(change)
|
||||
channel?.trySend(DragDelta(delta))
|
||||
}
|
||||
|
||||
coroutineScope {
|
||||
try {
|
||||
detectHorizontalDragGestures(
|
||||
canDrag = ::canDrag,
|
||||
onDragStart = onDragStart,
|
||||
onDragEnd = onDragEnd,
|
||||
onDragCancel = onDragCancel,
|
||||
shouldAwaitTouchSlop = shouldAwaitTouchSlop,
|
||||
onDrag = onDrag,
|
||||
)
|
||||
} catch (cancellation: CancellationException) {
|
||||
channel?.trySend(DragCancelled)
|
||||
if (!isActive) throw cancellation
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCancelPointerInput() {
|
||||
pointerInputNode?.onCancelPointerInput()
|
||||
}
|
||||
|
||||
fun update(
|
||||
enabled: Boolean = this.enabled,
|
||||
shouldResetPointerInputHandling: Boolean = false,
|
||||
) {
|
||||
var resetPointerInputHandling = shouldResetPointerInputHandling
|
||||
|
||||
if (this.enabled != enabled) {
|
||||
this.enabled = enabled
|
||||
if (!enabled) {
|
||||
pointerInputNode?.let { undelegate(it) }
|
||||
pointerInputNode = null
|
||||
}
|
||||
resetPointerInputHandling = true
|
||||
}
|
||||
|
||||
if (resetPointerInputHandling) {
|
||||
pointerInputNode?.resetPointerInputHandler()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class HorizontalDragEvent {
|
||||
class DragStarted(val startPoint: Offset) : HorizontalDragEvent()
|
||||
class DragStopped(val velocity: Velocity) : HorizontalDragEvent()
|
||||
object DragCancelled : HorizontalDragEvent()
|
||||
class DragDelta(val delta: Offset) : HorizontalDragEvent()
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
package androidx.compose.foundation.scrollbar
|
||||
|
||||
import androidx.compose.foundation.gestures.Orientation
|
||||
import androidx.compose.foundation.gestures.scrollBy
|
||||
import androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridItemInfo
|
||||
import androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
|
||||
internal class LazyStaggeredGridScrollbarAdapter(
|
||||
private val scrollState: LazyStaggeredGridState,
|
||||
) : LazyLineContentAdapter() {
|
||||
|
||||
override val viewportSize: Double
|
||||
get() = with(scrollState.layoutInfo) {
|
||||
if (isVertical) viewportSize.height else viewportSize.width
|
||||
}.toDouble()
|
||||
|
||||
private val isVertical by lazy {
|
||||
scrollState.layoutInfo.orientation == Orientation.Vertical
|
||||
}
|
||||
|
||||
private fun LazyStaggeredGridItemInfo.mainAxisSize() = with(size) {
|
||||
if (isVertical) height else width
|
||||
}
|
||||
|
||||
private fun LazyStaggeredGridItemInfo.mainAxisOffset() = with(offset) {
|
||||
if (isVertical) y else x
|
||||
}
|
||||
|
||||
override fun firstVisibleLine(): VisibleLine? {
|
||||
val first = scrollState.layoutInfo.visibleItemsInfo.firstOrNull()
|
||||
return first?.run { VisibleLine(index, mainAxisOffset()) }
|
||||
}
|
||||
|
||||
override fun totalLineCount() = scrollState.layoutInfo.totalItemsCount
|
||||
|
||||
override fun contentPadding() = with(scrollState.layoutInfo) {
|
||||
beforeContentPadding + afterContentPadding
|
||||
}
|
||||
|
||||
override suspend fun snapToLine(lineIndex: Int, scrollOffset: Int) {
|
||||
scrollState.scrollToItem(lineIndex, scrollOffset)
|
||||
}
|
||||
|
||||
override suspend fun scrollBy(value: Float) {
|
||||
scrollState.scrollBy(value)
|
||||
}
|
||||
|
||||
override fun averageVisibleLineSize() = with(scrollState.layoutInfo.visibleItemsInfo) {
|
||||
val first = firstOrNull() ?: return@with 0.0
|
||||
val last = last()
|
||||
(last.mainAxisOffset() + last.mainAxisSize() - first.mainAxisOffset()).toDouble() / size
|
||||
}
|
||||
|
||||
override val lineSpacing get() = 0
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun rememberScrollbarAdapter(
|
||||
scrollState: LazyStaggeredGridState,
|
||||
): ScrollbarAdapter = remember(scrollState) {
|
||||
LazyStaggeredGridScrollbarAdapter(scrollState)
|
||||
}
|
||||
@@ -0,0 +1,455 @@
|
||||
/*
|
||||
* Copyright 2023 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package androidx.compose.foundation.scrollbar
|
||||
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.core.TweenSpec
|
||||
import androidx.compose.foundation.ScrollState
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.gestures.awaitEachGesture
|
||||
import androidx.compose.foundation.gestures.awaitFirstDown
|
||||
import androidx.compose.foundation.gestures.drag
|
||||
import androidx.compose.foundation.hoverable
|
||||
import androidx.compose.foundation.interaction.DragInteraction
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.interaction.collectIsHoveredAsState
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.grid.LazyGridState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocal
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.staticCompositionLocalOf
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.composed
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.input.pointer.positionChange
|
||||
import androidx.compose.ui.layout.Layout
|
||||
import androidx.compose.ui.layout.MeasurePolicy
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import androidx.compose.ui.unit.Constraints
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import androidx.compose.ui.unit.constrainHeight
|
||||
import androidx.compose.ui.unit.constrainWidth
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
/**
|
||||
* [CompositionLocal] used to pass [ScrollbarStyle] down the tree.
|
||||
* This value is typically set in some "Theme" composable function
|
||||
* (DesktopTheme, MaterialTheme)
|
||||
*/
|
||||
val LocalScrollbarStyle = staticCompositionLocalOf { defaultScrollbarStyle() }
|
||||
|
||||
/**
|
||||
* Defines visual style of scrollbars (thickness, shapes, colors, etc).
|
||||
* Can be passed as a parameter of scrollbar through [LocalScrollbarStyle]
|
||||
*/
|
||||
@Immutable
|
||||
data class ScrollbarStyle(
|
||||
val minimalHeight: Dp,
|
||||
val thickness: Dp,
|
||||
val shape: Shape,
|
||||
val hoverDurationMillis: Int,
|
||||
val unhoverColor: Color,
|
||||
val hoverColor: Color,
|
||||
)
|
||||
|
||||
/**
|
||||
* Simple default [ScrollbarStyle] without applying MaterialTheme.
|
||||
*/
|
||||
fun defaultScrollbarStyle() = ScrollbarStyle(
|
||||
minimalHeight = 16.dp,
|
||||
thickness = 8.dp,
|
||||
shape = RoundedCornerShape(4.dp),
|
||||
hoverDurationMillis = 300,
|
||||
unhoverColor = Color.Black.copy(alpha = 0.12f),
|
||||
hoverColor = Color.Black.copy(alpha = 0.50f),
|
||||
)
|
||||
|
||||
/**
|
||||
* Vertical scrollbar that can be attached to some scrollable
|
||||
* component (ScrollableColumn, LazyColumn) and share common state with it.
|
||||
*
|
||||
* Can be placed independently.
|
||||
*
|
||||
* Example:
|
||||
* val state = rememberScrollState(0)
|
||||
*
|
||||
* Box(Modifier.fillMaxSize()) {
|
||||
* Box(modifier = Modifier.verticalScroll(state)) {
|
||||
* ...
|
||||
* }
|
||||
*
|
||||
* VerticalScrollbar(
|
||||
* adapter = rememberScrollbarAdapter(state)
|
||||
* modifier = Modifier.align(Alignment.CenterEnd).fillMaxHeight(),
|
||||
* )
|
||||
* }
|
||||
*
|
||||
* @param adapter [ScrollbarAdapter] that will be used to
|
||||
* communicate with scrollable component
|
||||
* @param modifier the modifier to apply to this layout
|
||||
* @param reverseLayout reverse the direction of scrolling and layout, when `true`
|
||||
* and [LazyListState.firstVisibleItemIndex] == 0 then scrollbar
|
||||
* will be at the bottom of the container.
|
||||
* It is usually used in pair with `LazyColumn(reverseLayout = true)`
|
||||
* @param style [ScrollbarStyle] to define visual style of scrollbar
|
||||
* @param interactionSource [MutableInteractionSource] that will be used to dispatch
|
||||
* [DragInteraction.Start] when this Scrollbar is being dragged.
|
||||
*/
|
||||
@Composable
|
||||
fun VerticalScrollbar(
|
||||
adapter: ScrollbarAdapter,
|
||||
modifier: Modifier = Modifier,
|
||||
reverseLayout: Boolean = false,
|
||||
style: ScrollbarStyle = LocalScrollbarStyle.current,
|
||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||
) = Scrollbar(
|
||||
adapter,
|
||||
modifier,
|
||||
reverseLayout,
|
||||
style,
|
||||
interactionSource,
|
||||
isVertical = true,
|
||||
)
|
||||
|
||||
/**
|
||||
* Horizontal scrollbar that can be attached to some scrollable
|
||||
* component (Modifier.verticalScroll(), LazyRow) and share common state with it.
|
||||
*
|
||||
* Can be placed independently.
|
||||
*
|
||||
* Example:
|
||||
* val state = rememberScrollState(0)
|
||||
*
|
||||
* Box(Modifier.fillMaxSize()) {
|
||||
* Box(modifier = Modifier.verticalScroll(state)) {
|
||||
* ...
|
||||
* }
|
||||
*
|
||||
* HorizontalScrollbar(
|
||||
* adapter = rememberScrollbarAdapter(state)
|
||||
* modifier = Modifier.align(Alignment.CenterEnd).fillMaxWidth(),
|
||||
* )
|
||||
* }
|
||||
*
|
||||
* @param adapter [ScrollbarAdapter] that will be used to
|
||||
* communicate with scrollable component
|
||||
* @param modifier the modifier to apply to this layout
|
||||
* @param reverseLayout reverse the direction of scrolling and layout, when `true`
|
||||
* and [LazyListState.firstVisibleItemIndex] == 0 then scrollbar
|
||||
* will be at the end of the container.
|
||||
* It is usually used in pair with `LazyRow(reverseLayout = true)`
|
||||
* @param style [ScrollbarStyle] to define visual style of scrollbar
|
||||
* @param interactionSource [MutableInteractionSource] that will be used to dispatch
|
||||
* [DragInteraction.Start] when this Scrollbar is being dragged.
|
||||
*/
|
||||
@Composable
|
||||
fun HorizontalScrollbar(
|
||||
adapter: ScrollbarAdapter,
|
||||
modifier: Modifier = Modifier,
|
||||
reverseLayout: Boolean = false,
|
||||
style: ScrollbarStyle = LocalScrollbarStyle.current,
|
||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||
) = Scrollbar(
|
||||
adapter,
|
||||
modifier,
|
||||
if (LocalLayoutDirection.current == LayoutDirection.Rtl) !reverseLayout else reverseLayout,
|
||||
style,
|
||||
interactionSource,
|
||||
isVertical = false,
|
||||
)
|
||||
|
||||
@Composable
|
||||
private fun Scrollbar(
|
||||
adapter: ScrollbarAdapter,
|
||||
modifier: Modifier = Modifier,
|
||||
reverseLayout: Boolean,
|
||||
style: ScrollbarStyle,
|
||||
interactionSource: MutableInteractionSource,
|
||||
isVertical: Boolean,
|
||||
) = with(LocalDensity.current) {
|
||||
val dragInteraction = remember<MutableState<DragInteraction.Start?>> { mutableStateOf(null) }
|
||||
DisposableEffect(key1 = interactionSource) {
|
||||
onDispose {
|
||||
dragInteraction.value?.let { interaction ->
|
||||
interactionSource.tryEmit(DragInteraction.Cancel(interaction))
|
||||
dragInteraction.value = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var containerSize by remember { mutableIntStateOf(0) }
|
||||
val isHovered by interactionSource.collectIsHoveredAsState()
|
||||
|
||||
val isHighlighted by remember {
|
||||
derivedStateOf {
|
||||
isHovered || dragInteraction.value is DragInteraction.Start
|
||||
}
|
||||
}
|
||||
|
||||
val minimalHeight = style.minimalHeight.toPx()
|
||||
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val sliderAdapter = remember(
|
||||
adapter,
|
||||
containerSize,
|
||||
minimalHeight,
|
||||
reverseLayout,
|
||||
isVertical,
|
||||
coroutineScope,
|
||||
) {
|
||||
SliderAdapter(
|
||||
adapter,
|
||||
containerSize,
|
||||
minimalHeight,
|
||||
reverseLayout = reverseLayout,
|
||||
isVertical = isVertical,
|
||||
coroutineScope = coroutineScope,
|
||||
)
|
||||
}
|
||||
|
||||
val scrollThickness = style.thickness.roundToPx()
|
||||
val measurePolicy = if (isVertical) {
|
||||
remember(sliderAdapter, scrollThickness) {
|
||||
verticalMeasurePolicy(sliderAdapter, { containerSize = it }, scrollThickness)
|
||||
}
|
||||
} else {
|
||||
remember(sliderAdapter, scrollThickness) {
|
||||
horizontalMeasurePolicy(sliderAdapter, { containerSize = it }, scrollThickness)
|
||||
}
|
||||
}
|
||||
|
||||
val color by animateColorAsState(
|
||||
if (isHighlighted) style.hoverColor else style.unhoverColor,
|
||||
animationSpec = TweenSpec(durationMillis = style.hoverDurationMillis),
|
||||
label = "Scrollbar color",
|
||||
)
|
||||
|
||||
val isVisible = sliderAdapter.thumbSize < containerSize
|
||||
|
||||
Layout(
|
||||
{
|
||||
Box(
|
||||
Modifier
|
||||
.background(if (isVisible) color else Color.Transparent, style.shape)
|
||||
.scrollbarDrag(
|
||||
interactionSource = interactionSource,
|
||||
draggedInteraction = dragInteraction,
|
||||
sliderAdapter = sliderAdapter,
|
||||
),
|
||||
)
|
||||
},
|
||||
modifier.hoverable(interactionSource = interactionSource),
|
||||
measurePolicy,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and [remember] [ScrollbarAdapter] for
|
||||
* scrollable container with the given instance [ScrollState].
|
||||
*/
|
||||
@Composable
|
||||
fun rememberScrollbarAdapter(
|
||||
scrollState: ScrollState,
|
||||
): ScrollbarAdapter = remember(scrollState) {
|
||||
ScrollbarAdapter(scrollState)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and [remember] [ScrollbarAdapter] for
|
||||
* lazy scrollable container with the given instance [LazyListState].
|
||||
*/
|
||||
@Composable
|
||||
fun rememberScrollbarAdapter(
|
||||
scrollState: LazyListState,
|
||||
): ScrollbarAdapter = remember(scrollState) {
|
||||
ScrollbarAdapter(scrollState)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and [remember] [ScrollbarAdapter] for lazy grid with
|
||||
* the given instance of [LazyGridState].
|
||||
*/
|
||||
@Composable
|
||||
fun rememberScrollbarAdapter(
|
||||
scrollState: LazyGridState,
|
||||
): ScrollbarAdapter = remember(scrollState) {
|
||||
ScrollbarAdapter(scrollState)
|
||||
}
|
||||
|
||||
/**
|
||||
* ScrollbarAdapter for Modifier.verticalScroll and Modifier.horizontalScroll
|
||||
*
|
||||
* [scrollState] is instance of [ScrollState] which is used by scrollable component
|
||||
*
|
||||
* Example:
|
||||
* val state = rememberScrollState(0)
|
||||
*
|
||||
* Box(Modifier.fillMaxSize()) {
|
||||
* Box(modifier = Modifier.verticalScroll(state)) {
|
||||
* ...
|
||||
* }
|
||||
*
|
||||
* VerticalScrollbar(
|
||||
* adapter = rememberScrollbarAdapter(state)
|
||||
* modifier = Modifier.align(Alignment.CenterEnd).fillMaxHeight(),
|
||||
* )
|
||||
* }
|
||||
*/
|
||||
fun ScrollbarAdapter(
|
||||
scrollState: ScrollState,
|
||||
): ScrollbarAdapter = ScrollableScrollbarAdapter(scrollState)
|
||||
|
||||
/**
|
||||
* ScrollbarAdapter for lazy lists.
|
||||
*
|
||||
* [scrollState] is instance of [LazyListState] which is used by scrollable component
|
||||
*
|
||||
* Example:
|
||||
* Box(Modifier.fillMaxSize()) {
|
||||
* val state = rememberLazyListState()
|
||||
*
|
||||
* LazyColumn(state = state) {
|
||||
* ...
|
||||
* }
|
||||
*
|
||||
* VerticalScrollbar(
|
||||
* adapter = rememberScrollbarAdapter(state)
|
||||
* modifier = Modifier.align(Alignment.CenterEnd).fillMaxHeight(),
|
||||
* )
|
||||
* }
|
||||
*/
|
||||
fun ScrollbarAdapter(
|
||||
scrollState: LazyListState,
|
||||
): ScrollbarAdapter = LazyListScrollbarAdapter(scrollState)
|
||||
|
||||
/**
|
||||
* ScrollbarAdapter for lazy grids.
|
||||
*
|
||||
* [scrollState] is instance of [LazyGridState] which is used by scrollable component
|
||||
*
|
||||
* Example:
|
||||
* Box(Modifier.fillMaxSize()) {
|
||||
* val state = rememberLazyGridState()
|
||||
*
|
||||
* LazyVerticalGrid(columns = ..., state = state) {
|
||||
* ...
|
||||
* }
|
||||
*
|
||||
* VerticalScrollbar(
|
||||
* adapter = rememberScrollbarAdapter(state)
|
||||
* modifier = Modifier.align(Alignment.CenterEnd).fillMaxHeight(),
|
||||
* )
|
||||
* }
|
||||
*/
|
||||
fun ScrollbarAdapter(
|
||||
scrollState: LazyGridState,
|
||||
): ScrollbarAdapter = LazyGridScrollbarAdapter(scrollState)
|
||||
|
||||
private val SliderAdapter.thumbPixelRange: IntRange
|
||||
get() {
|
||||
val start = position.roundToInt()
|
||||
val endExclusive = start + thumbSize.roundToInt()
|
||||
|
||||
return (start until endExclusive)
|
||||
}
|
||||
|
||||
private val IntRange.size get() = last + 1 - first
|
||||
|
||||
private fun verticalMeasurePolicy(
|
||||
sliderAdapter: SliderAdapter,
|
||||
setContainerSize: (Int) -> Unit,
|
||||
scrollThickness: Int,
|
||||
) = MeasurePolicy { measurables, constraints ->
|
||||
setContainerSize(constraints.maxHeight)
|
||||
val pixelRange = sliderAdapter.thumbPixelRange
|
||||
val placeable = measurables.first().measure(
|
||||
Constraints.fixed(
|
||||
constraints.constrainWidth(scrollThickness),
|
||||
pixelRange.size,
|
||||
),
|
||||
)
|
||||
layout(placeable.width, constraints.maxHeight) {
|
||||
placeable.place(0, pixelRange.first)
|
||||
}
|
||||
}
|
||||
|
||||
private fun horizontalMeasurePolicy(
|
||||
sliderAdapter: SliderAdapter,
|
||||
setContainerSize: (Int) -> Unit,
|
||||
scrollThickness: Int,
|
||||
) = MeasurePolicy { measurables, constraints ->
|
||||
setContainerSize(constraints.maxWidth)
|
||||
val pixelRange = sliderAdapter.thumbPixelRange
|
||||
val placeable = measurables.first().measure(
|
||||
Constraints.fixed(
|
||||
pixelRange.size,
|
||||
constraints.constrainHeight(scrollThickness),
|
||||
),
|
||||
)
|
||||
layout(constraints.maxWidth, placeable.height) {
|
||||
placeable.place(pixelRange.first, 0)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Modifier.scrollbarDrag(
|
||||
interactionSource: MutableInteractionSource,
|
||||
draggedInteraction: MutableState<DragInteraction.Start?>,
|
||||
sliderAdapter: SliderAdapter,
|
||||
): Modifier = composed {
|
||||
val currentInteractionSource by rememberUpdatedState(interactionSource)
|
||||
val currentDraggedInteraction by rememberUpdatedState(draggedInteraction)
|
||||
val currentSliderAdapter by rememberUpdatedState(sliderAdapter)
|
||||
|
||||
pointerInput(Unit) {
|
||||
awaitEachGesture {
|
||||
val down = awaitFirstDown(requireUnconsumed = false)
|
||||
val interaction = DragInteraction.Start()
|
||||
currentInteractionSource.tryEmit(interaction)
|
||||
currentDraggedInteraction.value = interaction
|
||||
currentSliderAdapter.onDragStarted()
|
||||
val isSuccess = drag(down.id) { change ->
|
||||
currentSliderAdapter.onDragDelta(change.positionChange())
|
||||
change.consume()
|
||||
}
|
||||
val finishInteraction = if (isSuccess) {
|
||||
DragInteraction.Stop(interaction)
|
||||
} else {
|
||||
DragInteraction.Cancel(interaction)
|
||||
}
|
||||
currentInteractionSource.tryEmit(finishInteraction)
|
||||
currentDraggedInteraction.value = null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,472 @@
|
||||
/*
|
||||
* Copyright 2023 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package androidx.compose.foundation.scrollbar
|
||||
|
||||
import androidx.compose.foundation.ScrollState
|
||||
import androidx.compose.foundation.gestures.Orientation
|
||||
import androidx.compose.foundation.gestures.scrollBy
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.grid.LazyGridItemInfo
|
||||
import androidx.compose.foundation.lazy.grid.LazyGridState
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.roundToInt
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.CoroutineStart
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
|
||||
/**
|
||||
* Defines how to scroll the scrollable component and how to display a scrollbar for it.
|
||||
*
|
||||
* The values of this interface are typically in pixels, but do not have to be.
|
||||
* It's possible to create an adapter with any scroll range of `Double` values.
|
||||
*/
|
||||
interface ScrollbarAdapter {
|
||||
|
||||
// We use `Double` values here in order to allow scrolling both very large (think LazyList with
|
||||
// millions of items) and very small (think something whose natural coordinates are less than 1)
|
||||
// content.
|
||||
|
||||
/**
|
||||
* Scroll offset of the content inside the scrollable component.
|
||||
*
|
||||
* For example, a value of `100` could mean the content is scrolled by 100 pixels from the
|
||||
* start.
|
||||
*/
|
||||
val scrollOffset: Double
|
||||
|
||||
/**
|
||||
* The size of the scrollable content, on the scrollable axis.
|
||||
*/
|
||||
val contentSize: Double
|
||||
|
||||
/**
|
||||
* The size of the viewport, on the scrollable axis.
|
||||
*/
|
||||
val viewportSize: Double
|
||||
|
||||
/**
|
||||
* Instantly jump to [scrollOffset].
|
||||
*
|
||||
* @param scrollOffset target offset to jump to, value will be coerced to the valid
|
||||
* scroll range.
|
||||
*/
|
||||
suspend fun scrollTo(scrollOffset: Double)
|
||||
}
|
||||
|
||||
/**
|
||||
* The maximum scroll offset of the scrollable content.
|
||||
*/
|
||||
val ScrollbarAdapter.maxScrollOffset: Double
|
||||
get() = (contentSize - viewportSize).coerceAtLeast(0.0)
|
||||
|
||||
internal class ScrollableScrollbarAdapter(
|
||||
private val scrollState: ScrollState,
|
||||
) : ScrollbarAdapter {
|
||||
|
||||
override val scrollOffset: Double get() = scrollState.value.toDouble()
|
||||
|
||||
override suspend fun scrollTo(scrollOffset: Double) {
|
||||
scrollState.scrollTo(scrollOffset.roundToInt())
|
||||
}
|
||||
|
||||
override val contentSize: Double
|
||||
// This isn't strictly correct, as the actual content can be smaller
|
||||
// than the viewport when scrollState.maxValue is 0, but the scrollbar
|
||||
// doesn't really care as long as contentSize <= viewportSize; it's
|
||||
// just not showing itself
|
||||
get() = scrollState.maxValue + viewportSize
|
||||
|
||||
override val viewportSize: Double
|
||||
get() = scrollState.viewportSize.toDouble()
|
||||
}
|
||||
|
||||
/**
|
||||
* Base class for [LazyListScrollbarAdapter] and [LazyGridScrollbarAdapter],
|
||||
* and in the future maybe other lazy widgets that lay out their content in lines.
|
||||
*/
|
||||
internal abstract class LazyLineContentAdapter : ScrollbarAdapter {
|
||||
|
||||
// Implement the adapter in terms of "lines", which means either rows,
|
||||
// (for a vertically scrollable widget) or columns (for a horizontally
|
||||
// scrollable one).
|
||||
// For LazyList this translates directly to items; for LazyGrid, it
|
||||
// translates to rows/columns of items.
|
||||
|
||||
class VisibleLine(
|
||||
val index: Int,
|
||||
val offset: Int,
|
||||
)
|
||||
|
||||
/**
|
||||
* Return the first visible line, if any.
|
||||
*/
|
||||
protected abstract fun firstVisibleLine(): VisibleLine?
|
||||
|
||||
/**
|
||||
* Return the total number of lines.
|
||||
*/
|
||||
protected abstract fun totalLineCount(): Int
|
||||
|
||||
/**
|
||||
* The sum of content padding (before+after) on the scrollable axis.
|
||||
*/
|
||||
protected abstract fun contentPadding(): Int
|
||||
|
||||
/**
|
||||
* Scroll immediately to the given line, and offset it by [scrollOffset] pixels.
|
||||
*/
|
||||
protected abstract suspend fun snapToLine(lineIndex: Int, scrollOffset: Int)
|
||||
|
||||
/**
|
||||
* Scroll from the current position by the given amount of pixels.
|
||||
*/
|
||||
protected abstract suspend fun scrollBy(value: Float)
|
||||
|
||||
/**
|
||||
* Return the average size (on the scrollable axis) of the visible lines.
|
||||
*/
|
||||
protected abstract fun averageVisibleLineSize(): Double
|
||||
|
||||
/**
|
||||
* The spacing between lines.
|
||||
*/
|
||||
protected abstract val lineSpacing: Int
|
||||
|
||||
private val averageVisibleLineSize by derivedStateOf {
|
||||
if (totalLineCount() == 0) {
|
||||
0.0
|
||||
} else {
|
||||
averageVisibleLineSize()
|
||||
}
|
||||
}
|
||||
|
||||
private val averageVisibleLineSizeWithSpacing get() = averageVisibleLineSize + lineSpacing
|
||||
|
||||
override val scrollOffset: Double
|
||||
get() {
|
||||
val firstVisibleLine = firstVisibleLine()
|
||||
return if (firstVisibleLine == null) {
|
||||
0.0
|
||||
} else {
|
||||
firstVisibleLine.index * averageVisibleLineSizeWithSpacing - firstVisibleLine.offset
|
||||
}
|
||||
}
|
||||
|
||||
override val contentSize: Double
|
||||
get() {
|
||||
val totalLineCount = totalLineCount()
|
||||
return averageVisibleLineSize * totalLineCount +
|
||||
lineSpacing * (totalLineCount - 1).coerceAtLeast(0) +
|
||||
contentPadding()
|
||||
}
|
||||
|
||||
override suspend fun scrollTo(scrollOffset: Double) {
|
||||
val distance = scrollOffset - this@LazyLineContentAdapter.scrollOffset
|
||||
|
||||
// if we scroll less than viewport we need to use scrollBy function to avoid
|
||||
// undesirable scroll jumps (when an item size is different)
|
||||
//
|
||||
// if we scroll more than viewport we should immediately jump to this position
|
||||
// without recreating all items between the current and the new position
|
||||
if (abs(distance) <= viewportSize) {
|
||||
scrollBy(distance.toFloat())
|
||||
} else {
|
||||
snapTo(scrollOffset)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun snapTo(scrollOffset: Double) {
|
||||
val scrollOffsetCoerced = scrollOffset.coerceIn(0.0, maxScrollOffset)
|
||||
|
||||
val index = (scrollOffsetCoerced / averageVisibleLineSizeWithSpacing)
|
||||
.toInt()
|
||||
.coerceAtLeast(0)
|
||||
.coerceAtMost(totalLineCount() - 1)
|
||||
|
||||
val offset = (scrollOffsetCoerced - index * averageVisibleLineSizeWithSpacing)
|
||||
.toInt()
|
||||
.coerceAtLeast(0)
|
||||
|
||||
snapToLine(lineIndex = index, scrollOffset = offset)
|
||||
}
|
||||
}
|
||||
|
||||
internal class LazyListScrollbarAdapter(
|
||||
private val scrollState: LazyListState,
|
||||
) : LazyLineContentAdapter() {
|
||||
|
||||
override val viewportSize: Double
|
||||
get() = with(scrollState.layoutInfo) {
|
||||
if (orientation == Orientation.Vertical) {
|
||||
viewportSize.height
|
||||
} else {
|
||||
viewportSize.width
|
||||
}
|
||||
}.toDouble()
|
||||
|
||||
/**
|
||||
* A heuristic that tries to ignore the "currently stickied" header because it breaks the other
|
||||
* computations in this adapter:
|
||||
* - The currently stickied header always appears in the list of visible items, with its
|
||||
* regular index. This makes [firstVisibleLine] always return its index, even if the list has
|
||||
* been scrolled far beyond it.
|
||||
* - [averageVisibleLineSize] calculates the average size in O(1) by assuming that items don't
|
||||
* overlap, and the stickied item breaks this assumption.
|
||||
*
|
||||
* Attempts to return the index into `visibleItemsInfo` of the first non-currently-stickied (it
|
||||
* could be sticky, but not stickied to the top of the list right now) item, if there is one.
|
||||
*
|
||||
* Note that this heuristic breaks down if the sticky header covers the entire list, so that
|
||||
* it's the only visible item for some portion of the scroll range. But there's currently no
|
||||
* known better way to solve it, and it's a relatively unusual case.
|
||||
*/
|
||||
private fun firstFloatingVisibleItemIndex() = with(scrollState.layoutInfo.visibleItemsInfo) {
|
||||
when (size) {
|
||||
0 -> null
|
||||
1 -> 0
|
||||
else -> {
|
||||
val first = this[0]
|
||||
val second = this[1]
|
||||
// If either the indices or the offsets aren't continuous, then the first item is
|
||||
// sticky, so we return 1
|
||||
if ((first.index < second.index - 1) ||
|
||||
(first.offset + first.size + lineSpacing > second.offset)
|
||||
) {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun firstVisibleLine(): VisibleLine? {
|
||||
val firstFloatingVisibleIndex = firstFloatingVisibleItemIndex() ?: return null
|
||||
val firstFloatingItem = scrollState.layoutInfo.visibleItemsInfo[firstFloatingVisibleIndex]
|
||||
return VisibleLine(
|
||||
index = firstFloatingItem.index,
|
||||
offset = firstFloatingItem.offset,
|
||||
)
|
||||
}
|
||||
|
||||
override fun totalLineCount() = scrollState.layoutInfo.totalItemsCount
|
||||
|
||||
override fun contentPadding() = with(scrollState.layoutInfo) {
|
||||
beforeContentPadding + afterContentPadding
|
||||
}
|
||||
|
||||
override suspend fun snapToLine(lineIndex: Int, scrollOffset: Int) {
|
||||
scrollState.scrollToItem(lineIndex, scrollOffset)
|
||||
}
|
||||
|
||||
override suspend fun scrollBy(value: Float) {
|
||||
scrollState.scrollBy(value)
|
||||
}
|
||||
|
||||
override fun averageVisibleLineSize() = with(scrollState.layoutInfo.visibleItemsInfo) {
|
||||
val firstFloatingIndex = firstFloatingVisibleItemIndex() ?: return@with 0.0
|
||||
val first = this[firstFloatingIndex]
|
||||
val last = last()
|
||||
val count = size - firstFloatingIndex
|
||||
(last.offset + last.size - first.offset - (count - 1) * lineSpacing).toDouble() / count
|
||||
}
|
||||
|
||||
override val lineSpacing get() = scrollState.layoutInfo.mainAxisItemSpacing
|
||||
}
|
||||
|
||||
internal class LazyGridScrollbarAdapter(
|
||||
private val scrollState: LazyGridState,
|
||||
) : LazyLineContentAdapter() {
|
||||
|
||||
override val viewportSize: Double
|
||||
get() = with(scrollState.layoutInfo) {
|
||||
if (isVertical) {
|
||||
viewportSize.height
|
||||
} else {
|
||||
viewportSize.width
|
||||
}
|
||||
}.toDouble()
|
||||
|
||||
private val isVertical by lazy {
|
||||
scrollState.layoutInfo.orientation == Orientation.Vertical
|
||||
}
|
||||
|
||||
private val unknownLine = with(LazyGridItemInfo) {
|
||||
if (isVertical) UnknownRow else UnknownColumn
|
||||
}
|
||||
|
||||
private fun LazyGridItemInfo.line() = if (isVertical) row else column
|
||||
|
||||
private fun LazyGridItemInfo.mainAxisSize() = with(size) {
|
||||
if (isVertical) height else width
|
||||
}
|
||||
|
||||
private fun LazyGridItemInfo.mainAxisOffset() = with(offset) {
|
||||
if (isVertical) y else x
|
||||
}
|
||||
|
||||
private fun slotsPerLine(): Int = scrollState.layoutInfo.visibleItemsInfo
|
||||
.groupingBy { it.line() }.eachCount().maxOfOrNull { it.value } ?: 1
|
||||
|
||||
private fun lineOfIndex(index: Int) = index / slotsPerLine()
|
||||
|
||||
private fun indexOfFirstInLine(line: Int) = line * slotsPerLine()
|
||||
|
||||
override fun firstVisibleLine(): VisibleLine? = scrollState.layoutInfo.visibleItemsInfo
|
||||
.firstOrNull { it.line() != unknownLine } // Skip exiting items
|
||||
?.let { firstVisibleItem ->
|
||||
VisibleLine(
|
||||
index = firstVisibleItem.line(),
|
||||
offset = firstVisibleItem.mainAxisOffset(),
|
||||
)
|
||||
}
|
||||
|
||||
override fun totalLineCount(): Int {
|
||||
val itemCount = scrollState.layoutInfo.totalItemsCount
|
||||
return if (itemCount == 0) {
|
||||
0
|
||||
} else {
|
||||
lineOfIndex(itemCount - 1) + 1
|
||||
}
|
||||
}
|
||||
|
||||
override fun contentPadding() = with(scrollState.layoutInfo) {
|
||||
beforeContentPadding + afterContentPadding
|
||||
}
|
||||
|
||||
override suspend fun snapToLine(lineIndex: Int, scrollOffset: Int) {
|
||||
scrollState.scrollToItem(
|
||||
index = indexOfFirstInLine(lineIndex),
|
||||
scrollOffset = scrollOffset,
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun scrollBy(value: Float) {
|
||||
scrollState.scrollBy(value)
|
||||
}
|
||||
|
||||
override fun averageVisibleLineSize(): Double {
|
||||
val visibleItemsInfo = // Non-exiting visible items
|
||||
scrollState.layoutInfo.visibleItemsInfo.filter { it.line() != unknownLine }
|
||||
if (visibleItemsInfo.isEmpty()) {
|
||||
return 0.0
|
||||
}
|
||||
|
||||
// Compute the size of the last line
|
||||
val last = visibleItemsInfo.last()
|
||||
val lastLine = last.line()
|
||||
val lastLineSize = visibleItemsInfo
|
||||
.asReversed()
|
||||
.asSequence()
|
||||
.takeWhile { it.line() == lastLine }
|
||||
.maxOf { it.mainAxisSize() }
|
||||
|
||||
val first = visibleItemsInfo.first()
|
||||
val lineCount = lastLine - first.line() + 1
|
||||
val lineSpacingSum = (lineCount - 1) * lineSpacing
|
||||
return (
|
||||
last.mainAxisOffset() + lastLineSize - first.mainAxisOffset() - lineSpacingSum
|
||||
).toDouble() / lineCount
|
||||
}
|
||||
|
||||
override val lineSpacing get() = scrollState.layoutInfo.mainAxisItemSpacing
|
||||
}
|
||||
|
||||
internal class SliderAdapter(
|
||||
private val adapter: ScrollbarAdapter,
|
||||
private val trackSize: Int,
|
||||
private val minHeight: Float,
|
||||
private val reverseLayout: Boolean,
|
||||
private val isVertical: Boolean,
|
||||
private val coroutineScope: CoroutineScope,
|
||||
) {
|
||||
|
||||
private val contentSize get() = adapter.contentSize
|
||||
private val visiblePart: Double
|
||||
get() {
|
||||
val contentSize = contentSize
|
||||
return if (contentSize == 0.0) {
|
||||
1.0
|
||||
} else {
|
||||
(adapter.viewportSize / contentSize).coerceAtMost(1.0)
|
||||
}
|
||||
}
|
||||
|
||||
val thumbSize
|
||||
get() = (trackSize * visiblePart).coerceAtLeast(minHeight.toDouble())
|
||||
|
||||
private val scrollScale: Double
|
||||
get() {
|
||||
val extraScrollbarSpace = trackSize - thumbSize
|
||||
val extraContentSpace = adapter.maxScrollOffset // == contentSize - viewportSize
|
||||
return if (extraContentSpace == 0.0) 1.0 else extraScrollbarSpace / extraContentSpace
|
||||
}
|
||||
|
||||
private val rawPosition: Double
|
||||
get() = scrollScale * adapter.scrollOffset
|
||||
|
||||
val position: Double
|
||||
get() = if (reverseLayout) trackSize - thumbSize - rawPosition else rawPosition
|
||||
|
||||
// How much of the current drag was ignored because we've reached the end of the scrollbar area
|
||||
private var unscrolledDragDistance = 0.0
|
||||
|
||||
/** Called when the thumb dragging starts */
|
||||
fun onDragStarted() {
|
||||
unscrolledDragDistance = 0.0
|
||||
}
|
||||
|
||||
private suspend fun setPosition(value: Double) {
|
||||
val rawPosition = if (reverseLayout) {
|
||||
trackSize - thumbSize - value
|
||||
} else {
|
||||
value
|
||||
}
|
||||
adapter.scrollTo(rawPosition / scrollScale)
|
||||
}
|
||||
|
||||
private val dragMutex = Mutex()
|
||||
|
||||
/** Called on every movement while dragging the thumb */
|
||||
fun onDragDelta(offset: Offset) {
|
||||
coroutineScope.launch(start = CoroutineStart.UNDISPATCHED) {
|
||||
// Mutex is used to ensure that all earlier drag deltas were applied
|
||||
// before calculating a new raw position
|
||||
dragMutex.withLock {
|
||||
val dragDelta = if (isVertical) offset.y else offset.x
|
||||
val maxScrollPosition = adapter.maxScrollOffset * scrollScale
|
||||
val currentPosition = position
|
||||
val targetPosition =
|
||||
(currentPosition + dragDelta + unscrolledDragDistance).coerceIn(
|
||||
0.0,
|
||||
maxScrollPosition,
|
||||
)
|
||||
val sliderDelta = targetPosition - currentPosition
|
||||
|
||||
// Have to add to position for smooth content scroll if the items are of different size
|
||||
val newPos = position + sliderDelta
|
||||
setPosition(newPos)
|
||||
unscrolledDragDistance += dragDelta - sliderDelta
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -76,13 +76,11 @@ fun SearchBarInputField(
|
||||
singleLine = true,
|
||||
visualTransformation = VisualTransformation.None,
|
||||
interactionSource = interactionSource,
|
||||
label = label,
|
||||
placeholder = placeholder,
|
||||
leadingIcon = leadingIcon?.let { leading -> { Box(Modifier.offset(x = SearchBarIconOffsetX)) { leading() } } },
|
||||
trailingIcon = trailingIcon?.let { trailing -> { Box(Modifier.offset(x = -SearchBarIconOffsetX)) { trailing() } } },
|
||||
shape = SearchBarDefaults.inputFieldShape,
|
||||
colors = colors,
|
||||
contentPadding = TextFieldDefaults.contentPaddingWithoutLabel(),
|
||||
container = {},
|
||||
)
|
||||
},
|
||||
@@ -102,5 +100,4 @@ fun SearchBarInputField(
|
||||
val SearchBarIconOffsetX = 4.dp
|
||||
val SearchBarMinWidth = 360.dp
|
||||
val SearchBarMaxWidth = 720.dp
|
||||
val SearchBarHorizontalPadding = 16.dp
|
||||
const val AnimationDelayMillis = 100
|
||||
@@ -1,20 +1,17 @@
|
||||
@file:Suppress("ktlint:standard:property-naming", "ktlint:standard:function-naming")
|
||||
@file:Suppress("ktlint:standard:property-naming")
|
||||
|
||||
package androidx.compose.material3
|
||||
|
||||
import androidx.compose.animation.core.AnimationSpec
|
||||
import androidx.compose.animation.core.TweenSpec
|
||||
import androidx.compose.animation.core.animate
|
||||
import androidx.compose.animation.core.exponentialDecay
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.gestures.AnchoredDraggableDefaults
|
||||
import androidx.compose.foundation.gestures.AnchoredDraggableState
|
||||
import androidx.compose.foundation.gestures.DraggableAnchors
|
||||
import androidx.compose.foundation.gestures.awaitEachGesture
|
||||
import androidx.compose.foundation.gestures.awaitFirstDown
|
||||
import androidx.compose.foundation.gestures.awaitHorizontalTouchSlopOrCancellation
|
||||
import androidx.compose.foundation.gestures.anchoredHorizontalDraggable
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.foundation.gestures.horizontalDrag
|
||||
import androidx.compose.foundation.gestures.snapTo
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
@@ -42,17 +39,12 @@ import androidx.compose.ui.graphics.RectangleShape
|
||||
import androidx.compose.ui.graphics.TileMode
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.input.pointer.positionChange
|
||||
import androidx.compose.ui.input.pointer.util.VelocityTracker
|
||||
import androidx.compose.ui.input.pointer.util.addPointerInputChange
|
||||
import androidx.compose.ui.layout.layout
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import androidx.compose.ui.unit.Constraints
|
||||
import androidx.compose.ui.unit.Density
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.lerp
|
||||
import com.hippo.ehviewer.ui.tools.animateFloatMergeOneWayPredictiveBackAsState
|
||||
@@ -61,9 +53,6 @@ import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
private val AnimationSpec = TweenSpec<Float>(durationMillis = 256)
|
||||
private const val DrawerPositionalThreshold = 0.5f
|
||||
private val DrawerVelocityThreshold = 400.dp
|
||||
private val ContainerWidth = 360.0.dp
|
||||
|
||||
@Suppress("NotCloseable")
|
||||
@Stable
|
||||
@@ -74,11 +63,7 @@ class DrawerState2(
|
||||
|
||||
val anchoredDraggableState = AnchoredDraggableState(
|
||||
initialValue = initialValue,
|
||||
snapAnimationSpec = AnimationSpec,
|
||||
decayAnimationSpec = exponentialDecay(7.5f),
|
||||
confirmValueChange = confirmStateChange,
|
||||
positionalThreshold = { distance -> distance * DrawerPositionalThreshold },
|
||||
velocityThreshold = { with(requireDensity()) { DrawerVelocityThreshold.toPx() } },
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -239,10 +224,8 @@ class DrawerState2(
|
||||
fun rememberDrawerState2(
|
||||
initialValue: DrawerValue,
|
||||
confirmStateChange: (DrawerValue) -> Boolean = { true },
|
||||
): DrawerState2 {
|
||||
return rememberSaveable(saver = DrawerState2.Saver(confirmStateChange)) {
|
||||
DrawerState2(initialValue, confirmStateChange)
|
||||
}
|
||||
): DrawerState2 = rememberSaveable(saver = DrawerState2.Saver(confirmStateChange)) {
|
||||
DrawerState2(initialValue, confirmStateChange)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -256,60 +239,34 @@ fun ModalSideDrawer(
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val density = LocalDensity.current
|
||||
var minValue by remember { mutableFloatStateOf(with(density) { ContainerWidth.toPx() }) }
|
||||
val maxValue = 0f
|
||||
var maxValue by remember { mutableFloatStateOf(with(density) { DrawerDefaults.MaximumDrawerWidth.toPx() }) }
|
||||
val minValue = 0f
|
||||
|
||||
SideEffect {
|
||||
drawerState.density = density
|
||||
drawerState.anchoredDraggableState.updateAnchors(
|
||||
DraggableAnchors {
|
||||
DrawerValue.Closed at minValue
|
||||
DrawerValue.Open at maxValue
|
||||
DrawerValue.Closed at maxValue
|
||||
DrawerValue.Open at minValue
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
|
||||
val velocityTracker = remember { VelocityTracker() }
|
||||
val dragModifier = if (gesturesEnabled) {
|
||||
Modifier.pointerInput(isRtl) {
|
||||
val multiplier = if (isRtl) -1 else 1
|
||||
awaitEachGesture {
|
||||
fun canConsume(slop: Float) = (slop > 0 && drawerState.isOpen) || (slop < 0 && drawerState.isClosed)
|
||||
val down = awaitFirstDown(requireUnconsumed = false)
|
||||
var ignore = false
|
||||
val drag = awaitHorizontalTouchSlopOrCancellation(down.id) { change, over ->
|
||||
val overSlop = over * multiplier
|
||||
val canConsume = canConsume(overSlop)
|
||||
if (canConsume && !ignore) {
|
||||
velocityTracker.addPointerInputChange(change)
|
||||
change.consume()
|
||||
drawerState.anchoredDraggableState.dispatchRawDelta(overSlop)
|
||||
} else {
|
||||
ignore = true
|
||||
}
|
||||
}
|
||||
if (drag != null) {
|
||||
horizontalDrag(drag.id) {
|
||||
velocityTracker.addPointerInputChange(it)
|
||||
drag.consume()
|
||||
drawerState.anchoredDraggableState.dispatchRawDelta(it.positionChange().x * multiplier)
|
||||
}
|
||||
val velocity = velocityTracker.calculateVelocity()
|
||||
velocityTracker.resetTracking()
|
||||
scope.launch {
|
||||
drawerState.anchoredDraggableState.settle(velocity.x * multiplier)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
Box(modifier.fillMaxSize().then(dragModifier)) {
|
||||
Box(
|
||||
modifier.fillMaxSize().anchoredHorizontalDraggable(
|
||||
state = drawerState.anchoredDraggableState,
|
||||
enableDragFromStartToEnd = drawerState.isOpen,
|
||||
enableDragFromEndToStart = drawerState.isClosed,
|
||||
enabled = gesturesEnabled,
|
||||
flingBehavior = AnchoredDraggableDefaults.flingBehavior(
|
||||
state = drawerState.anchoredDraggableState,
|
||||
animationSpec = AnimationSpec,
|
||||
),
|
||||
),
|
||||
) {
|
||||
val radius by remember {
|
||||
snapshotFlow {
|
||||
val step = calculateFraction(minValue, maxValue, drawerState.currentOffset)
|
||||
val step = calculateFraction(maxValue, minValue, drawerState.currentOffset)
|
||||
lerp(0, 10, step).dp
|
||||
}
|
||||
}.collectAsState(0.dp)
|
||||
@@ -331,7 +288,7 @@ fun ModalSideDrawer(
|
||||
}
|
||||
},
|
||||
fraction = {
|
||||
calculateFraction(minValue, maxValue, drawerState.requireOffset())
|
||||
calculateFraction(maxValue, minValue, drawerState.requireOffset())
|
||||
},
|
||||
color = scrimColor,
|
||||
)
|
||||
@@ -349,7 +306,7 @@ fun ModalSideDrawer(
|
||||
val scaledHeight = (multiplierY * placeable.height).roundToInt()
|
||||
val reMeasured = measurable.measure(Constraints.fixed(scaledWidth, scaledHeight))
|
||||
layout(scaledWidth, scaledHeight) {
|
||||
reMeasured.placeRelative(-(scaledWidth - placeable.width), 0)
|
||||
reMeasured.placeRelative(placeable.width - scaledWidth, 0)
|
||||
}
|
||||
}.background(
|
||||
color = MaterialTheme.colorScheme.surfaceColorAtElevation(absoluteElevation),
|
||||
@@ -361,14 +318,20 @@ fun ModalSideDrawer(
|
||||
scaleX = scale
|
||||
scaleY = scale
|
||||
}.offset {
|
||||
IntOffset(lerp(0f, minValue * 0.05f, predictiveState).roundToInt(), 0)
|
||||
IntOffset(lerp(0f, maxValue * 0.05f, predictiveState).roundToInt(), 0)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Modifier.onGloballyPositioned {
|
||||
val w = it.size.width
|
||||
if (w != 0) {
|
||||
minValue = it.size.width.toFloat()
|
||||
Modifier.onSizeChanged {
|
||||
val width = it.width
|
||||
if (width != 0) {
|
||||
maxValue = width.toFloat()
|
||||
drawerState.anchoredDraggableState.updateAnchors(
|
||||
DraggableAnchors {
|
||||
DrawerValue.Closed at maxValue
|
||||
DrawerValue.Open at minValue
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,242 @@
|
||||
/*
|
||||
* Copyright 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package androidx.compose.material3.fork
|
||||
|
||||
import androidx.annotation.FloatRange
|
||||
import androidx.compose.animation.core.SpringSpec
|
||||
import androidx.compose.foundation.gestures.AnchoredDraggableDefaults
|
||||
import androidx.compose.foundation.gestures.AnchoredDraggableState
|
||||
import androidx.compose.foundation.gestures.DraggableAnchors
|
||||
import androidx.compose.foundation.gestures.Orientation
|
||||
import androidx.compose.foundation.gestures.anchoredHorizontalDraggable
|
||||
import androidx.compose.foundation.gestures.animateTo
|
||||
import androidx.compose.foundation.gestures.draggableAnchors
|
||||
import androidx.compose.foundation.gestures.snapTo
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.material3.SwipeToDismissBoxDefaults
|
||||
import androidx.compose.material3.SwipeToDismissBoxValue
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.Saver
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.Density
|
||||
import kotlinx.coroutines.CancellationException
|
||||
|
||||
private val SnapAnimationSpec = SpringSpec<Float>()
|
||||
|
||||
/**
|
||||
* State of the [SwipeToDismissBox] composable.
|
||||
*
|
||||
* @param initialValue The initial value of the state.
|
||||
* @param density The density that this state can use to convert values to and from dp.
|
||||
* @param confirmValueChange Optional callback invoked to confirm or veto a pending state change.
|
||||
* while a swipe is in progress and when settling after the swipe ends. This is the distance from
|
||||
* the start of a transition. It will be, depending on the direction of the interaction, added or
|
||||
* subtracted from/to the origin offset. It should always be a positive value.
|
||||
*/
|
||||
class SwipeToDismissBoxState(
|
||||
initialValue: SwipeToDismissBoxValue,
|
||||
internal val density: Density,
|
||||
confirmValueChange: (SwipeToDismissBoxValue) -> Boolean = { true },
|
||||
) {
|
||||
internal val anchoredDraggableState =
|
||||
AnchoredDraggableState(
|
||||
initialValue = initialValue,
|
||||
confirmValueChange = confirmValueChange,
|
||||
)
|
||||
|
||||
internal val offset: Float get() = anchoredDraggableState.offset
|
||||
|
||||
/**
|
||||
* Require the current offset.
|
||||
*
|
||||
* @throws IllegalStateException If the offset has not been initialized yet
|
||||
*/
|
||||
fun requireOffset(): Float = anchoredDraggableState.requireOffset()
|
||||
|
||||
/**
|
||||
* The current state value of the [SwipeToDismissBoxState].
|
||||
*/
|
||||
val currentValue: SwipeToDismissBoxValue get() = anchoredDraggableState.currentValue
|
||||
|
||||
/**
|
||||
* The target state. This is the closest state to the current offset (taking into account
|
||||
* positional thresholds). If no interactions like animations or drags are in progress, this
|
||||
* will be the current state.
|
||||
*/
|
||||
val targetValue: SwipeToDismissBoxValue get() = anchoredDraggableState.targetValue
|
||||
|
||||
/**
|
||||
* The fraction of the progress going from currentValue to targetValue, within [0f..1f] bounds.
|
||||
*/
|
||||
@get:FloatRange(from = 0.0, to = 1.0)
|
||||
val progress: Float get() = anchoredDraggableState.progress(anchoredDraggableState.settledValue, anchoredDraggableState.targetValue)
|
||||
|
||||
/**
|
||||
* The direction (if any) in which the composable has been or is being dismissed.
|
||||
*
|
||||
* Use this to change the background of the [SwipeToDismissBox] if you want different actions
|
||||
* on each side.
|
||||
*/
|
||||
val dismissDirection: SwipeToDismissBoxValue
|
||||
get() = when {
|
||||
offset == 0f || offset.isNaN() -> SwipeToDismissBoxValue.Settled
|
||||
offset > 0f -> SwipeToDismissBoxValue.StartToEnd
|
||||
else -> SwipeToDismissBoxValue.EndToStart
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the state without any animation and suspend until it's set
|
||||
*
|
||||
* @param targetValue The new target value
|
||||
*/
|
||||
suspend fun snapTo(targetValue: SwipeToDismissBoxValue) {
|
||||
anchoredDraggableState.snapTo(targetValue)
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the component to the default position with animation and suspend until it if fully
|
||||
* reset or animation has been cancelled. This method will throw [CancellationException] if
|
||||
* the animation is interrupted
|
||||
*
|
||||
* @return the reason the reset animation ended
|
||||
*/
|
||||
suspend fun reset() = anchoredDraggableState.animateTo(
|
||||
targetValue = SwipeToDismissBoxValue.Settled,
|
||||
)
|
||||
|
||||
/**
|
||||
* Dismiss the component in the given [direction], with an animation and suspend. This method
|
||||
* will throw [CancellationException] if the animation is interrupted
|
||||
*
|
||||
* @param direction The dismiss direction.
|
||||
*/
|
||||
suspend fun dismiss(direction: SwipeToDismissBoxValue) {
|
||||
anchoredDraggableState.animateTo(targetValue = direction)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
/**
|
||||
* The default [Saver] implementation for [SwipeToDismissBoxState].
|
||||
*/
|
||||
fun Saver(
|
||||
confirmValueChange: (SwipeToDismissBoxValue) -> Boolean,
|
||||
density: Density,
|
||||
) = Saver<SwipeToDismissBoxState, SwipeToDismissBoxValue>(
|
||||
save = { it.currentValue },
|
||||
restore = {
|
||||
SwipeToDismissBoxState(
|
||||
it,
|
||||
density,
|
||||
confirmValueChange,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and [remember] a [SwipeToDismissBoxState].
|
||||
*
|
||||
* @param initialValue The initial value of the state.
|
||||
* @param confirmValueChange Optional callback invoked to confirm or veto a pending state change.
|
||||
* while a swipe is in progress and when settling after the swipe ends. This is the distance from
|
||||
* the start of a transition. It will be, depending on the direction of the interaction, added or
|
||||
* subtracted from/to the origin offset. It should always be a positive value.
|
||||
*/
|
||||
@Composable
|
||||
fun rememberSwipeToDismissBoxState(
|
||||
initialValue: SwipeToDismissBoxValue = SwipeToDismissBoxValue.Settled,
|
||||
confirmValueChange: (SwipeToDismissBoxValue) -> Boolean = { true },
|
||||
): SwipeToDismissBoxState {
|
||||
val density = LocalDensity.current
|
||||
return rememberSaveable(
|
||||
saver = SwipeToDismissBoxState.Saver(
|
||||
confirmValueChange = confirmValueChange,
|
||||
density = density,
|
||||
),
|
||||
) {
|
||||
SwipeToDismissBoxState(initialValue, density, confirmValueChange)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A composable that can be dismissed by swiping left or right.
|
||||
*
|
||||
* @param state The state of this component.
|
||||
* @param backgroundContent A composable that is stacked behind the [content] and is exposed when
|
||||
* the content is swiped. You can/should use the [state] to have different backgrounds on each side.
|
||||
* @param modifier Optional [Modifier] for this component.
|
||||
* @param enableDismissFromStartToEnd Whether SwipeToDismissBox can be dismissed from start to end.
|
||||
* @param enableDismissFromEndToStart Whether SwipeToDismissBox can be dismissed from end to start.
|
||||
* @param gesturesEnabled Whether swipe-to-dismiss can be interacted by gestures.
|
||||
* @param content The content that can be dismissed.
|
||||
*/
|
||||
@Composable
|
||||
fun SwipeToDismissBox(
|
||||
state: SwipeToDismissBoxState,
|
||||
backgroundContent: @Composable RowScope.() -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
enableDismissFromStartToEnd: Boolean = true,
|
||||
enableDismissFromEndToStart: Boolean = true,
|
||||
gesturesEnabled: Boolean = true,
|
||||
content: @Composable RowScope.() -> Unit,
|
||||
) {
|
||||
Box(
|
||||
modifier
|
||||
.anchoredHorizontalDraggable(
|
||||
state = state.anchoredDraggableState,
|
||||
enableDragFromStartToEnd = enableDismissFromStartToEnd,
|
||||
enableDragFromEndToStart = enableDismissFromEndToStart,
|
||||
enabled = gesturesEnabled && state.currentValue == SwipeToDismissBoxValue.Settled,
|
||||
flingBehavior = AnchoredDraggableDefaults.flingBehavior(
|
||||
state = state.anchoredDraggableState,
|
||||
positionalThreshold = SwipeToDismissBoxDefaults.positionalThreshold,
|
||||
animationSpec = SnapAnimationSpec,
|
||||
),
|
||||
),
|
||||
propagateMinConstraints = true,
|
||||
) {
|
||||
Row(
|
||||
content = backgroundContent,
|
||||
modifier = Modifier.matchParentSize(),
|
||||
)
|
||||
Row(
|
||||
content = content,
|
||||
modifier = Modifier.draggableAnchors(
|
||||
state.anchoredDraggableState,
|
||||
Orientation.Horizontal,
|
||||
) { size, _ ->
|
||||
val width = size.width.toFloat()
|
||||
return@draggableAnchors DraggableAnchors {
|
||||
SwipeToDismissBoxValue.Settled at 0f
|
||||
if (enableDismissFromStartToEnd) {
|
||||
SwipeToDismissBoxValue.StartToEnd at width
|
||||
}
|
||||
if (enableDismissFromEndToStart) {
|
||||
SwipeToDismissBoxValue.EndToStart at -width
|
||||
}
|
||||
} to state.targetValue
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
-1710
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,163 @@
|
||||
/*
|
||||
* Copyright 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.accompanist.systemuicontroller
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
import android.view.View
|
||||
import android.view.Window
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.compose.ui.window.DialogWindowProvider
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
|
||||
/**
|
||||
* A class which provides easy-to-use utilities for updating the System UI bar
|
||||
* colors within Jetpack Compose.
|
||||
*/
|
||||
@Stable
|
||||
interface SystemUiController {
|
||||
|
||||
/**
|
||||
* Control for the behavior of the system bars. This value should be one of the
|
||||
* [WindowInsetsControllerCompat] behavior constants:
|
||||
* [WindowInsetsControllerCompat.BEHAVIOR_SHOW_BARS_BY_TOUCH] (Deprecated),
|
||||
* [WindowInsetsControllerCompat.BEHAVIOR_DEFAULT] and
|
||||
* [WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE].
|
||||
*/
|
||||
var systemBarsBehavior: Int
|
||||
|
||||
/**
|
||||
* Property which holds the status bar visibility. If set to true, show the status bar,
|
||||
* otherwise hide the status bar.
|
||||
*/
|
||||
var isStatusBarVisible: Boolean
|
||||
|
||||
/**
|
||||
* Property which holds the navigation bar visibility. If set to true, show the navigation bar,
|
||||
* otherwise hide the navigation bar.
|
||||
*/
|
||||
var isNavigationBarVisible: Boolean
|
||||
|
||||
/**
|
||||
* Property which holds the status & navigation bar visibility. If set to true, show both bars,
|
||||
* otherwise hide both bars.
|
||||
*/
|
||||
var isSystemBarsVisible: Boolean
|
||||
get() = isNavigationBarVisible && isStatusBarVisible
|
||||
set(value) {
|
||||
isStatusBarVisible = value
|
||||
isNavigationBarVisible = value
|
||||
}
|
||||
|
||||
/**
|
||||
* Property which holds whether the status bar icons + content are 'dark' or not.
|
||||
*/
|
||||
var statusBarDarkContentEnabled: Boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Remembers a [SystemUiController] for the given [window].
|
||||
*
|
||||
* If no [window] is provided, an attempt to find the correct [Window] is made.
|
||||
*
|
||||
* First, if the [LocalView]'s parent is a [DialogWindowProvider], then that dialog's [Window] will
|
||||
* be used.
|
||||
*
|
||||
* Second, we attempt to find [Window] for the [Activity] containing the [LocalView].
|
||||
*
|
||||
* If none of these are found (such as may happen in a preview), then the functionality of the
|
||||
* returned [SystemUiController] will be degraded, but won't throw an exception.
|
||||
*/
|
||||
@Composable
|
||||
fun rememberSystemUiController(
|
||||
window: Window? = findWindow(),
|
||||
): SystemUiController {
|
||||
val view = LocalView.current
|
||||
return remember(view, window) { AndroidSystemUiController(view, window) }
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun findWindow(): Window? =
|
||||
(LocalView.current.parent as? DialogWindowProvider)?.window
|
||||
?: LocalView.current.context.findWindow()
|
||||
|
||||
private tailrec fun Context.findWindow(): Window? =
|
||||
when (this) {
|
||||
is Activity -> window
|
||||
is ContextWrapper -> baseContext.findWindow()
|
||||
else -> null
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper class for setting the navigation and status bar colors for a [View], gracefully
|
||||
* degrading behavior based upon API level.
|
||||
*
|
||||
* Typically you would use [rememberSystemUiController] to remember an instance of this.
|
||||
*/
|
||||
internal class AndroidSystemUiController(
|
||||
private val view: View,
|
||||
window: Window?,
|
||||
) : SystemUiController {
|
||||
private val windowInsetsController = window?.let {
|
||||
WindowCompat.getInsetsController(it, view)
|
||||
}
|
||||
|
||||
override var systemBarsBehavior: Int
|
||||
get() = windowInsetsController?.systemBarsBehavior ?: 0
|
||||
set(value) {
|
||||
windowInsetsController?.systemBarsBehavior = value
|
||||
}
|
||||
|
||||
override var isStatusBarVisible: Boolean
|
||||
get() {
|
||||
return ViewCompat.getRootWindowInsets(view)
|
||||
?.isVisible(WindowInsetsCompat.Type.statusBars()) == true
|
||||
}
|
||||
set(value) {
|
||||
if (value) {
|
||||
windowInsetsController?.show(WindowInsetsCompat.Type.statusBars())
|
||||
} else {
|
||||
windowInsetsController?.hide(WindowInsetsCompat.Type.statusBars())
|
||||
}
|
||||
}
|
||||
|
||||
override var isNavigationBarVisible: Boolean
|
||||
get() {
|
||||
return ViewCompat.getRootWindowInsets(view)
|
||||
?.isVisible(WindowInsetsCompat.Type.navigationBars()) == true
|
||||
}
|
||||
set(value) {
|
||||
if (value) {
|
||||
windowInsetsController?.show(WindowInsetsCompat.Type.navigationBars())
|
||||
} else {
|
||||
windowInsetsController?.hide(WindowInsetsCompat.Type.navigationBars())
|
||||
}
|
||||
}
|
||||
|
||||
override var statusBarDarkContentEnabled: Boolean
|
||||
get() = windowInsetsController?.isAppearanceLightStatusBars == true
|
||||
set(value) {
|
||||
windowInsetsController?.isAppearanceLightStatusBars = value
|
||||
}
|
||||
}
|
||||
@@ -22,15 +22,14 @@ import android.os.StrictMode
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.collection.LruCache
|
||||
import androidx.compose.runtime.snapshots.Snapshot
|
||||
import androidx.lifecycle.ProcessLifecycleOwner
|
||||
import androidx.lifecycle.coroutineScope
|
||||
import coil3.EventListener
|
||||
import coil3.SingletonImageLoader
|
||||
import coil3.asCoilImage
|
||||
import coil3.asImage
|
||||
import coil3.gif.AnimatedImageDecoder
|
||||
import coil3.gif.GifDecoder
|
||||
import coil3.network.ktor.KtorNetworkFetcherFactory
|
||||
import coil3.network.ktor3.KtorNetworkFetcherFactory
|
||||
import coil3.request.ErrorResult
|
||||
import coil3.request.ImageRequest
|
||||
import coil3.request.crossfade
|
||||
@@ -40,22 +39,30 @@ import com.hippo.ehviewer.client.EhCookieStore
|
||||
import com.hippo.ehviewer.client.EhTagDatabase
|
||||
import com.hippo.ehviewer.client.data.GalleryDetail
|
||||
import com.hippo.ehviewer.coil.CropBorderInterceptor
|
||||
import com.hippo.ehviewer.coil.DetectBorderInterceptor
|
||||
import com.hippo.ehviewer.coil.DownloadThumbInterceptor
|
||||
import com.hippo.ehviewer.coil.HardwareBitmapInterceptor
|
||||
import com.hippo.ehviewer.coil.MapExtraInfoInterceptor
|
||||
import com.hippo.ehviewer.coil.MergeInterceptor
|
||||
import com.hippo.ehviewer.coil.QrCodeInterceptor
|
||||
import com.hippo.ehviewer.cronet.cronetHttpClient
|
||||
import com.hippo.ehviewer.dailycheck.checkDawn
|
||||
import com.hippo.ehviewer.dao.SearchDatabase
|
||||
import com.hippo.ehviewer.download.DownloadManager
|
||||
import com.hippo.ehviewer.download.DownloadsFilterMode
|
||||
import com.hippo.ehviewer.ktbuilder.diskCache
|
||||
import com.hippo.ehviewer.ktbuilder.imageLoader
|
||||
import com.hippo.ehviewer.ktor.CronetEngine
|
||||
import com.hippo.ehviewer.ktor.Cronet
|
||||
import com.hippo.ehviewer.legacy.cleanObsoleteCache
|
||||
import com.hippo.ehviewer.ui.keepNoMediaFileStatus
|
||||
import com.hippo.ehviewer.ui.lockObserver
|
||||
import com.hippo.ehviewer.ui.tools.dataStateFlow
|
||||
import com.hippo.ehviewer.ui.tools.initSETConnection
|
||||
import com.hippo.ehviewer.util.AppConfig
|
||||
import com.hippo.ehviewer.util.Crash
|
||||
import com.hippo.ehviewer.util.FavouriteStatusRouter
|
||||
import com.hippo.ehviewer.util.FileUtils
|
||||
import com.hippo.ehviewer.util.isAtLeastO
|
||||
import com.hippo.ehviewer.util.isAtLeastP
|
||||
import com.hippo.ehviewer.util.isAtLeastS
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
@@ -63,6 +70,7 @@ import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.plugins.cookies.HttpCookies
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import logcat.AndroidLogcatLogger
|
||||
import logcat.LogPriority
|
||||
@@ -75,8 +83,11 @@ import splitties.init.appCtx
|
||||
private val lifecycle = ProcessLifecycleOwner.get().lifecycle
|
||||
private val lifecycleScope = lifecycle.coroutineScope
|
||||
|
||||
class EhApplication : Application(), SingletonImageLoader.Factory {
|
||||
class EhApplication :
|
||||
Application(),
|
||||
SingletonImageLoader.Factory {
|
||||
override fun onCreate() {
|
||||
initSETConnection()
|
||||
// Initialize Settings on first access
|
||||
lifecycleScope.launchIO {
|
||||
val mode = Settings.theme
|
||||
@@ -103,13 +114,13 @@ class EhApplication : Application(), SingletonImageLoader.Factory {
|
||||
super.onCreate()
|
||||
System.loadLibrary("ehviewer")
|
||||
lifecycleScope.launchIO {
|
||||
launch { EhTagDatabase }
|
||||
launch { EhDB }
|
||||
dataStateFlow.value
|
||||
launch {
|
||||
EhTagDatabase
|
||||
}
|
||||
launch {
|
||||
EhDB
|
||||
}
|
||||
launch {
|
||||
if (DownloadManager.labelList.isNotEmpty() && Settings.downloadFilterMode.key !in Settings.prefs) {
|
||||
Settings.downloadFilterMode.value = DownloadsFilterMode.CUSTOM.flag
|
||||
}
|
||||
DownloadManager.readMetadataFromLocal()
|
||||
}
|
||||
launch {
|
||||
@@ -130,9 +141,6 @@ class EhApplication : Application(), SingletonImageLoader.Factory {
|
||||
}
|
||||
if (BuildConfig.DEBUG) {
|
||||
StrictMode.enableDefaults()
|
||||
Snapshot.registerApplyObserver { anies, _ ->
|
||||
logcat(LogPriority.VERBOSE) { anies.toString() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,13 +169,19 @@ class EhApplication : Application(), SingletonImageLoader.Factory {
|
||||
}
|
||||
|
||||
override fun newImageLoader(context: Context) = context.imageLoader {
|
||||
interceptorCoroutineContext(Dispatchers.Default)
|
||||
components {
|
||||
serviceLoaderEnabled(false)
|
||||
add(KtorNetworkFetcherFactory { ktorClient })
|
||||
add(MergeInterceptor)
|
||||
add(DownloadThumbInterceptor)
|
||||
if (isAtLeastO) {
|
||||
add(HardwareBitmapInterceptor)
|
||||
}
|
||||
add(CropBorderInterceptor)
|
||||
add(HardwareBitmapInterceptor)
|
||||
add(DetectBorderInterceptor)
|
||||
add(QrCodeInterceptor)
|
||||
add(MapExtraInfoInterceptor)
|
||||
if (isAtLeastP) {
|
||||
add(AnimatedImageDecoder.Factory(false))
|
||||
} else {
|
||||
@@ -177,7 +191,7 @@ class EhApplication : Application(), SingletonImageLoader.Factory {
|
||||
diskCache { imageCache }
|
||||
crossfade(300)
|
||||
val drawable = AppCompatResources.getDrawable(appCtx, R.drawable.image_failed)
|
||||
if (drawable != null) error(drawable.asCoilImage(true))
|
||||
if (drawable != null) error(drawable.asImage(true))
|
||||
if (BuildConfig.DEBUG) {
|
||||
logger(DebugLogger())
|
||||
} else {
|
||||
@@ -193,7 +207,10 @@ class EhApplication : Application(), SingletonImageLoader.Factory {
|
||||
|
||||
companion object {
|
||||
val ktorClient by lazy {
|
||||
HttpClient(CronetEngine) {
|
||||
HttpClient(Cronet) {
|
||||
engine {
|
||||
client = cronetHttpClient
|
||||
}
|
||||
install(HttpCookies) {
|
||||
storage = EhCookieStore
|
||||
}
|
||||
@@ -201,7 +218,7 @@ class EhApplication : Application(), SingletonImageLoader.Factory {
|
||||
}
|
||||
|
||||
val noRedirectKtorClient by lazy {
|
||||
HttpClient(CronetEngine) {
|
||||
HttpClient(ktorClient.engine) {
|
||||
followRedirects = false
|
||||
install(HttpCookies) {
|
||||
storage = EhCookieStore
|
||||
|
||||
@@ -20,6 +20,7 @@ import android.net.Uri
|
||||
import arrow.fx.coroutines.release
|
||||
import arrow.fx.coroutines.resource
|
||||
import com.hippo.ehviewer.client.data.BaseGalleryInfo
|
||||
import com.hippo.ehviewer.dao.DownloadArtist
|
||||
import com.hippo.ehviewer.dao.DownloadDirname
|
||||
import com.hippo.ehviewer.dao.DownloadInfo
|
||||
import com.hippo.ehviewer.dao.DownloadLabel
|
||||
@@ -32,8 +33,9 @@ import com.hippo.ehviewer.dao.QuickSearch
|
||||
import com.hippo.ehviewer.dao.Schema17to18
|
||||
import com.hippo.ehviewer.download.DownloadManager
|
||||
import com.hippo.ehviewer.util.sendTo
|
||||
import com.hippo.unifile.asUniFile
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import okio.Path
|
||||
import okio.Path.Companion.toOkioPath
|
||||
import splitties.arch.room.roomDb
|
||||
|
||||
object EhDB {
|
||||
@@ -112,8 +114,11 @@ object EhDB {
|
||||
dao.insertOrIgnore(downloadDirnameList)
|
||||
}
|
||||
|
||||
val downloadsCount
|
||||
get() = db.downloadsDao().count()
|
||||
val downloadsCountByLabel
|
||||
get() = db.downloadsDao().countByLabel()
|
||||
|
||||
val downloadsCountByArtist
|
||||
get() = db.downloadsDao().countByArtist()
|
||||
|
||||
suspend fun getAllDownloadLabelList() = db.downloadLabelDao().list()
|
||||
|
||||
@@ -141,6 +146,14 @@ object EhDB {
|
||||
dao.fill(raw.position)
|
||||
}
|
||||
|
||||
suspend fun putDownloadArtist(gid: Long, artists: List<DownloadArtist>) {
|
||||
if (artists.isNotEmpty()) {
|
||||
val dao = db.downloadArtistDao()
|
||||
dao.deleteByGid(gid)
|
||||
dao.insertOrIgnore(artists)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun removeLocalFavorites(galleryInfo: BaseGalleryInfo) {
|
||||
db.localFavoritesDao().deleteByKey(galleryInfo.gid)
|
||||
deleteGalleryInfo(galleryInfo)
|
||||
@@ -260,10 +273,10 @@ object EhDB {
|
||||
db.filterDao().update(filter)
|
||||
}
|
||||
|
||||
fun exportDB(context: Context, uri: Uri) {
|
||||
fun exportDB(context: Context, file: Path) {
|
||||
db.query("PRAGMA wal_checkpoint(FULL)", null).use { it.moveToNext() }
|
||||
val dbFile = context.getDatabasePath(DB_NAME)
|
||||
dbFile.asUniFile() sendTo uri.asUniFile()
|
||||
dbFile.toOkioPath() sendTo file
|
||||
}
|
||||
|
||||
suspend fun importDB(context: Context, uri: Uri) {
|
||||
@@ -278,49 +291,39 @@ object EhDB {
|
||||
it.close()
|
||||
context.deleteDatabase(tempDBName)
|
||||
} use { oldDB ->
|
||||
runCatching {
|
||||
db.galleryDao().insertOrIgnore(oldDB.galleryDao().list())
|
||||
db.galleryDao().insertOrIgnore(oldDB.galleryDao().list())
|
||||
|
||||
db.progressDao().insertOrIgnore(oldDB.progressDao().list())
|
||||
|
||||
val downloadLabelList = oldDB.downloadLabelDao().list()
|
||||
DownloadManager.addDownloadLabel(downloadLabelList)
|
||||
|
||||
oldDB.downloadDirnameDao().list().let {
|
||||
importDownloadDirname(it)
|
||||
}
|
||||
runCatching {
|
||||
db.progressDao().insertOrIgnore(oldDB.progressDao().list())
|
||||
|
||||
val downloadInfoList = oldDB.downloadsDao().joinList().asReversed()
|
||||
DownloadManager.addDownload(downloadInfoList)
|
||||
|
||||
val historyInfoList = oldDB.historyDao().list()
|
||||
importHistoryInfo(historyInfoList)
|
||||
|
||||
val quickSearchList = oldDB.quickSearchDao().list()
|
||||
val currentQuickSearchList = db.quickSearchDao().list()
|
||||
val offset = currentQuickSearchList.size
|
||||
val importList = quickSearchList.filter { newQS ->
|
||||
currentQuickSearchList.none { it.name == newQS.name }
|
||||
}.onEachIndexed { index, q -> q.position = index + offset }
|
||||
importQuickSearch(importList)
|
||||
|
||||
oldDB.localFavoritesDao().list().let {
|
||||
importLocalFavorites(it)
|
||||
}
|
||||
runCatching {
|
||||
val downloadLabelList = oldDB.downloadLabelDao().list()
|
||||
DownloadManager.addDownloadLabel(downloadLabelList)
|
||||
}
|
||||
runCatching {
|
||||
oldDB.downloadDirnameDao().list().let {
|
||||
importDownloadDirname(it)
|
||||
}
|
||||
}
|
||||
runCatching {
|
||||
val downloadInfoList = oldDB.downloadsDao().joinList().asReversed()
|
||||
DownloadManager.addDownload(downloadInfoList)
|
||||
}
|
||||
runCatching {
|
||||
val historyInfoList = oldDB.historyDao().list()
|
||||
importHistoryInfo(historyInfoList)
|
||||
}
|
||||
runCatching {
|
||||
val quickSearchList = oldDB.quickSearchDao().list()
|
||||
val currentQuickSearchList = db.quickSearchDao().list()
|
||||
val offset = currentQuickSearchList.size
|
||||
val importList = quickSearchList.filter { newQS ->
|
||||
currentQuickSearchList.none { it.name == newQS.name }
|
||||
}.onEachIndexed { index, q -> q.position = index + offset }
|
||||
importQuickSearch(importList)
|
||||
}
|
||||
runCatching {
|
||||
oldDB.localFavoritesDao().list().let {
|
||||
importLocalFavorites(it)
|
||||
}
|
||||
}
|
||||
runCatching {
|
||||
val filterList = oldDB.filterDao().list()
|
||||
val currentFilterList = db.filterDao().list()
|
||||
filterList.forEach {
|
||||
if (it !in currentFilterList) addFilter(it)
|
||||
}
|
||||
|
||||
val filterList = oldDB.filterDao().list()
|
||||
val currentFilterList = db.filterDao().list()
|
||||
filterList.forEach {
|
||||
if (it !in currentFilterList) addFilter(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,10 +9,13 @@ import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.remember
|
||||
import com.hippo.ehviewer.client.CHROME_MOBILE_USER_AGENT
|
||||
import com.hippo.ehviewer.client.CHROME_USER_AGENT
|
||||
import com.hippo.ehviewer.client.EhUtils
|
||||
import com.hippo.ehviewer.client.EhCookieStore
|
||||
import com.hippo.ehviewer.client.data.FavListUrlBuilder
|
||||
import com.hippo.ehviewer.download.DownloadsFilterMode
|
||||
import com.hippo.ehviewer.download.SortMode
|
||||
import com.hippo.ehviewer.util.AppConfig
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
|
||||
import java.util.Locale
|
||||
@@ -98,6 +101,7 @@ fun <T> PrefDelegate<T>.asMutableState(): MutableState<T> {
|
||||
object Settings : DataStorePreferences(null) {
|
||||
private const val KEY_SHOW_TAG_TRANSLATIONS = "show_tag_translations"
|
||||
|
||||
@Suppress("ktlint:standard:backing-property-naming")
|
||||
private val _favFlow = MutableSharedFlow<Unit>()
|
||||
val favChangesFlow = _favFlow.debounce(1000)
|
||||
var favCat by stringArrayPref("fav_cat", 10, "Favorites").emitTo(_favFlow)
|
||||
@@ -110,12 +114,16 @@ object Settings : DataStorePreferences(null) {
|
||||
val listThumbSize = intPref("list_tile_size", 40)
|
||||
val languageFilter = intPref("language_filter", -1)
|
||||
val downloadSortMode = intPref("download_sort_mode", SortMode.Default.flag)
|
||||
val downloadFilterMode = intPref("download_filter_mode", DownloadsFilterMode.Default.flag)
|
||||
val hasSignedIn = boolPref("has_signed_in", EhCookieStore.hasSignedIn())
|
||||
val meteredNetworkWarning = boolPref("cellular_network_warning", false)
|
||||
val blackDarkTheme = boolPref("black_dark_theme", false)
|
||||
val gridView = boolPref("grid_view", false)
|
||||
val showGalleryPages = boolPref("show_gallery_pages", true)
|
||||
val qSSaveProgress = boolPref("qs_save_progress", true)
|
||||
val security = boolPref("require_unlock", false)
|
||||
val animateItems = boolPref("animate_items", true)
|
||||
val displayName = stringOrNullPref("display_name", null)
|
||||
val recentDownloadLabel = stringOrNullPref("recent_download_label", null)
|
||||
|
||||
var downloadScheme by stringOrNullPref("image_scheme", null)
|
||||
@@ -134,7 +142,7 @@ object Settings : DataStorePreferences(null) {
|
||||
var readCacheSize by intPref("read_cache_size_2", 640)
|
||||
var launchPage by intPref("launch_page_2", 0)
|
||||
var commentThreshold by intPref("comment_threshold", -101)
|
||||
var forceEhThumb by boolPref("force_eh_thumb", false)
|
||||
var hardwareBitmapThreshold by intPref("hardware_bitmap_threshold", 16384)
|
||||
var showComments by boolPref("show_gallery_comments", true)
|
||||
var requestNews by boolPref("request_news", false).observed { updateWhenRequestNewsChanges() }
|
||||
var hideHvEvents by boolPref("hide_hv_events", false)
|
||||
@@ -142,7 +150,8 @@ object Settings : DataStorePreferences(null) {
|
||||
var showTagTranslations by boolPref(KEY_SHOW_TAG_TRANSLATIONS, false).observed { updateWhenTagTranslationChanges() }
|
||||
var appLinkVerifyTip by boolPref("app_link_verify_tip", false)
|
||||
var enabledSecurity by boolPref("enable_secure", false)
|
||||
var useCIUpdateChannel by boolPref("ci_update_channel", false)
|
||||
var backupBeforeUpdate by boolPref("backup_before_update", true)
|
||||
var useCIUpdateChannel by boolPref("ci_update_channel", AppConfig.isSnapshot)
|
||||
var mediaScan by boolPref("media_scan", false).observed { updateWhenKeepMediaStatusChanges() }
|
||||
var hasDefaultDownloadLabel by boolPref("has_default_download_label", false)
|
||||
var saveParseErrorBody by boolPref("save_parse_error_body", true)
|
||||
@@ -154,29 +163,19 @@ object Settings : DataStorePreferences(null) {
|
||||
var downloadOriginImage by boolPref("download_origin_image", false)
|
||||
var saveAsCbz by boolPref("save_as_cbz", false)
|
||||
var archiveMetadata by boolPref("archive_metadata", true)
|
||||
var desktopSite by boolPref("desktop_site", false)
|
||||
var recentFavCat by intPref("recent_fav_cat", FavListUrlBuilder.FAV_CAT_LOCAL)
|
||||
var defaultFavSlot by intPref("default_favorite_slot", -2)
|
||||
var securityDelay by intPref("require_unlock_delay", 0)
|
||||
var clipboardTextHashCode by intPref("clipboard_text_hash_code", 0)
|
||||
var searchCategory by intPref("search_pref", EhUtils.ALL_CATEGORY)
|
||||
var requestNewsTime by intPref("request_news_time", 0).observed { updateWhenRequestNewsChanges() }
|
||||
var updateIntervalDays by intPref("update_interval_days", 7)
|
||||
var lastDawnDays by intPref("last_dawn_days", 0)
|
||||
var recentToplist by stringPref("recent_toplist", "11")
|
||||
var userAgent by stringPref("user_agent", CHROME_USER_AGENT)
|
||||
var defaultDownloadLabel by stringOrNullPref("default_download_label", null)
|
||||
var displayName by stringOrNullPref("display_name", null)
|
||||
var avatar by stringOrNullPref("avatar", null)
|
||||
var language by stringPref("app_language", "system").observed { updateWhenLocaleChanges() }
|
||||
var lastUpdateTime by longPref("last_update_time", 0)
|
||||
var favDialogTheta by floatPref("fav_select_dialog_delta", 0F)
|
||||
|
||||
// TODO: Remove this after swipe gestures are correctly handled in compose
|
||||
var touchSlopFactor by intPref("touch_slop", 3)
|
||||
|
||||
// Tachiyomi Reader
|
||||
var newReader by boolPref("new_compose_reader", false)
|
||||
var lastUpdateTime by longPref("last_update_time", BuildConfig.COMMIT_TIME)
|
||||
|
||||
// Reader
|
||||
val cropBorder = boolPref("crop_borders", false)
|
||||
val colorFilter = boolPref("pref_color_filter_key", false)
|
||||
val colorFilterValue = intPref("color_filter_value", 0)
|
||||
@@ -201,12 +200,19 @@ object Settings : DataStorePreferences(null) {
|
||||
val invertedColors = boolPref("pref_inverted_colors", false)
|
||||
val readerWebtoonNav = intPref("reader_navigation_mode_webtoon", 0)
|
||||
val readerPagerNav = intPref("reader_navigation_mode_pager", 0)
|
||||
val readerWebtoonNavInverted = intPref("reader_tapping_inverted_webtoon", 0)
|
||||
val readerPagerNavInverted = intPref("reader_tapping_inverted_2", 0)
|
||||
val readerWebtoonNavInverted = intPref("reader_tapping_inverted_webtoon_2", 0)
|
||||
val webtoonSidePadding = intPref("webtoon_side_padding", 0)
|
||||
val navigateToPan = boolPref("navigate_pan", true)
|
||||
val imageScaleType = intPref("pref_image_scale_type_key", 1)
|
||||
val landscapeZoom = boolPref("landscape_zoom", false)
|
||||
val zoomStart = intPref("pref_zoom_start_key", 1)
|
||||
val showNavigationOverlayNewUser = boolPref("reader_navigation_overlay_new_user", true)
|
||||
val showNavigationOverlayOnStart = boolPref("reader_navigation_overlay_on_start", false)
|
||||
val stripExtraneousAds = boolPref("strip_extraneous_ads", false)
|
||||
|
||||
val userAgent
|
||||
get() = if (desktopSite) CHROME_USER_AGENT else CHROME_MOBILE_USER_AGENT
|
||||
|
||||
init {
|
||||
if ("CN" == Locale.getDefault().country) {
|
||||
@@ -214,6 +220,9 @@ object Settings : DataStorePreferences(null) {
|
||||
if (KEY_SHOW_TAG_TRANSLATIONS !in prefs) showTagTranslations = true
|
||||
}
|
||||
}
|
||||
if (downloadFilterMode.value == DownloadsFilterMode.ARTIST.flag && recentDownloadLabel.value == null) {
|
||||
recentDownloadLabel.value = ""
|
||||
}
|
||||
}
|
||||
|
||||
interface Delegate<R> {
|
||||
@@ -225,7 +234,7 @@ object Settings : DataStorePreferences(null) {
|
||||
private fun intArrayPref(key: String, count: Int) = object : Delegate<IntArray> {
|
||||
override val flowGetter: () -> Flow<Unit> = { _value.asFlow().flatMapMerge { it.changesFlow() }.conflate() }
|
||||
|
||||
@Suppress("ktlint:standard:property-naming")
|
||||
@Suppress("ktlint:standard:backing-property-naming")
|
||||
private var _value = (0 until count).map { intPref("${key}_$it", 0) }.toTypedArray()
|
||||
override fun getValue(thisRef: Any?, prop: KProperty<*>?): IntArray = _value.map { it.value }.toIntArray()
|
||||
override fun setValue(thisRef: Any?, prop: KProperty<*>?, value: IntArray) {
|
||||
@@ -237,7 +246,7 @@ object Settings : DataStorePreferences(null) {
|
||||
private fun stringArrayPref(key: String, count: Int, defMetaValue: String) = object : Delegate<Array<String>> {
|
||||
override val flowGetter: () -> Flow<Unit> = { _value.asFlow().flatMapMerge { it.changesFlow() }.conflate() }
|
||||
|
||||
@Suppress("ktlint:standard:property-naming")
|
||||
@Suppress("ktlint:standard:backing-property-naming")
|
||||
private var _value = (0 until count).map { stringPref("${key}_$it", "$defMetaValue $it") }.toTypedArray()
|
||||
override fun getValue(thisRef: Any?, prop: KProperty<*>?): Array<String> = _value.map { it.value }.toTypedArray()
|
||||
override fun setValue(thisRef: Any?, prop: KProperty<*>?, value: Array<String>) {
|
||||
|
||||
@@ -2,8 +2,6 @@ package com.hippo.ehviewer
|
||||
|
||||
import android.app.UiModeManager
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.core.os.LocaleListCompat
|
||||
import com.hippo.ehviewer.client.EhCookieStore
|
||||
import com.hippo.ehviewer.client.EhEngine
|
||||
import com.hippo.ehviewer.client.EhTagDatabase
|
||||
import com.hippo.ehviewer.dailycheck.updateDailyCheckWork
|
||||
@@ -27,15 +25,6 @@ fun <T, R : Settings.Delegate<T>> R.observed(func: (Unit) -> Unit) = apply { col
|
||||
fun <T, R : Settings.Delegate<T>> R.emitTo(flow: MutableSharedFlow<Unit>) = apply { collectScope.launch { flowGetter().collect { flow.emit(Unit) } } }
|
||||
fun <T, R : PrefDelegate<T>> R.emitTo(flow: MutableSharedFlow<Unit>) = apply { collectScope.launch { changesFlow().collect { flow.emit(Unit) } } }
|
||||
|
||||
fun updateWhenLocaleChanges() {
|
||||
val newValue = Settings.language
|
||||
if ("system" == newValue) {
|
||||
AppCompatDelegate.setApplicationLocales(LocaleListCompat.getEmptyLocaleList())
|
||||
} else {
|
||||
AppCompatDelegate.setApplicationLocales(LocaleListCompat.forLanguageTags(newValue))
|
||||
}
|
||||
}
|
||||
|
||||
fun updateWhenKeepMediaStatusChanges() {
|
||||
collectScope.launchIO {
|
||||
runCatching {
|
||||
@@ -64,7 +53,7 @@ fun updateWhenRequestNewsChanges() {
|
||||
}
|
||||
|
||||
fun updateWhenGallerySiteChanges() {
|
||||
if (!Settings.needSignIn && EhCookieStore.hasSignedIn()) {
|
||||
if (Settings.hasSignedIn.value) {
|
||||
collectScope.launchIO {
|
||||
runCatching {
|
||||
EhEngine.getUConfig()
|
||||
|
||||
@@ -15,36 +15,35 @@
|
||||
*/
|
||||
package com.hippo.ehviewer.client
|
||||
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.client.data.GalleryInfo
|
||||
import com.hippo.ehviewer.client.data.GalleryPreview
|
||||
import com.hippo.ehviewer.client.data.NormalGalleryPreview
|
||||
|
||||
// Normal Preview: https://*.hath.network/cm/[timed token]/[gid]-[index].jpg
|
||||
// ExHentai Large Preview: https://s.exhentai.org/t/***
|
||||
// ExHentai Normal Preview: https://s.exhentai.org/m/***
|
||||
// EHentai Normal Preview: https://ehgt.org/m/***
|
||||
// EHentai Large Preview: https://ehgt.org/***
|
||||
// E-Hentai Large Preview: https://ehgt.org/***
|
||||
|
||||
private const val URL_PREFIX_THUMB_E = "https://ehgt.org/"
|
||||
private const val URL_PREFIX_THUMB_EX = "https://s.exhentai.org/"
|
||||
private const val URL_PREFIX_LARGE_THUMB_EX = URL_PREFIX_THUMB_EX + "t/"
|
||||
private const val URL_PREFIX_THUMB_EX = "https://s.exhentai.org/t/"
|
||||
private const val NORMAL_PREVIEW_PREFIX = "$"
|
||||
private val NormalPreviewKeyRegex = Regex("/(\\d+-\\d+)\\.jpg$")
|
||||
|
||||
fun getImageKey(gid: Long, index: Int): String {
|
||||
return "image:$gid:$index"
|
||||
}
|
||||
fun getImageKey(gid: Long, index: Int): String = "image:$gid:$index"
|
||||
|
||||
fun getThumbKey(url: String): String {
|
||||
return url.removePrefix(URL_PREFIX_THUMB_E).removePrefix(URL_PREFIX_LARGE_THUMB_EX).removePrefix(URL_PREFIX_THUMB_EX)
|
||||
}
|
||||
fun getThumbKey(url: String): String = url.removePrefix(thumbPrefix)
|
||||
|
||||
val GalleryPreview.url
|
||||
get() = if (exThumb) {
|
||||
if (this is NormalGalleryPreview) URL_PREFIX_THUMB_EX else URL_PREFIX_LARGE_THUMB_EX
|
||||
val String.isNormalPreviewKey
|
||||
get() = startsWith(NORMAL_PREVIEW_PREFIX)
|
||||
|
||||
val GalleryPreview.imageKey
|
||||
get() = if (this is NormalGalleryPreview) {
|
||||
NormalPreviewKeyRegex.find(url)?.run { NORMAL_PREVIEW_PREFIX + groupValues[1] }
|
||||
} else {
|
||||
URL_PREFIX_THUMB_E
|
||||
} + imageKey
|
||||
getThumbKey(url)
|
||||
}
|
||||
|
||||
val GalleryInfo.thumbUrl
|
||||
get() = (if (exThumb) URL_PREFIX_LARGE_THUMB_EX else URL_PREFIX_THUMB_E) + thumbKey!!
|
||||
get() = thumbPrefix + EhUtils.handleThumbUrlResolution(thumbKey!!)
|
||||
|
||||
val exThumb get() = EhUtils.isExHentai && !Settings.forceEhThumb
|
||||
private val thumbPrefix
|
||||
get() = if (EhUtils.isExHentai) URL_PREFIX_THUMB_EX else URL_PREFIX_THUMB_E
|
||||
@@ -1,24 +1,19 @@
|
||||
package com.hippo.ehviewer.client
|
||||
|
||||
import android.webkit.CookieManager
|
||||
import androidx.compose.ui.util.fastAny
|
||||
import androidx.compose.ui.util.fastJoinToString
|
||||
import io.ktor.client.plugins.cookies.CookiesStorage
|
||||
import io.ktor.http.Cookie
|
||||
import io.ktor.http.Url
|
||||
import io.ktor.http.parseClientCookiesHeader
|
||||
import io.ktor.http.renderCookieHeader
|
||||
import io.ktor.http.renderSetCookieHeader
|
||||
|
||||
object EhCookieStore : CookiesStorage {
|
||||
private val manager = CookieManager.getInstance()
|
||||
fun signOut() = manager.removeAllCookies(null)
|
||||
fun contains(url: String, name: String) = load(Url(url)).fastAny { it.name == name }
|
||||
fun removeAllCookies() = manager.removeAllCookies(null)
|
||||
|
||||
fun hasSignedIn(): Boolean {
|
||||
val url = EhUrl.HOST_E
|
||||
return contains(url, KEY_IPB_MEMBER_ID) && contains(url, KEY_IPB_PASS_HASH)
|
||||
}
|
||||
fun hasSignedIn(): Boolean = getCookies(EhUrl.HOST_E)?.run {
|
||||
containsKey(KEY_IPB_MEMBER_ID) && containsKey(KEY_IPB_PASS_HASH)
|
||||
} ?: false
|
||||
|
||||
const val KEY_IPB_MEMBER_ID = "ipb_member_id"
|
||||
const val KEY_IPB_PASS_HASH = "ipb_pass_hash"
|
||||
@@ -53,11 +48,7 @@ object EhCookieStore : CookiesStorage {
|
||||
|
||||
fun flush() = manager.flush()
|
||||
|
||||
fun getCookieHeader(url: String): String {
|
||||
return load(Url(url)).fastJoinToString("; ") {
|
||||
renderCookieHeader(it)
|
||||
}
|
||||
}
|
||||
fun getCookieHeader(url: String): String? = manager.getCookie(url)
|
||||
|
||||
// See https://github.com/Ehviewer-Overhauled/Ehviewer/issues/873
|
||||
override suspend fun addCookie(requestUrl: Url, cookie: Cookie) {
|
||||
|
||||
@@ -17,6 +17,7 @@ package com.hippo.ehviewer.client
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.left
|
||||
import arrow.core.partially2
|
||||
import arrow.core.right
|
||||
import arrow.fx.coroutines.parMap
|
||||
import arrow.fx.coroutines.parZip
|
||||
@@ -28,6 +29,7 @@ import com.hippo.ehviewer.client.data.FavListUrlBuilder
|
||||
import com.hippo.ehviewer.client.data.GalleryInfo
|
||||
import com.hippo.ehviewer.client.exception.EhException
|
||||
import com.hippo.ehviewer.client.exception.InsufficientFundsException
|
||||
import com.hippo.ehviewer.client.exception.NoHitsFoundException
|
||||
import com.hippo.ehviewer.client.exception.NotLoggedInException
|
||||
import com.hippo.ehviewer.client.exception.ParseException
|
||||
import com.hippo.ehviewer.client.parser.ArchiveParser
|
||||
@@ -44,24 +46,29 @@ import com.hippo.ehviewer.client.parser.GalleryTokenApiParser
|
||||
import com.hippo.ehviewer.client.parser.HomeParser
|
||||
import com.hippo.ehviewer.client.parser.ProfileParser
|
||||
import com.hippo.ehviewer.client.parser.RateGalleryResult
|
||||
import com.hippo.ehviewer.client.parser.SignInParser
|
||||
import com.hippo.ehviewer.client.parser.TorrentParser
|
||||
import com.hippo.ehviewer.client.parser.TorrentResult
|
||||
import com.hippo.ehviewer.client.parser.UserConfigParser
|
||||
import com.hippo.ehviewer.client.parser.VoteCommentResult
|
||||
import com.hippo.ehviewer.client.parser.VoteTagParser
|
||||
import com.hippo.ehviewer.dailycheck.showEventNotification
|
||||
import com.hippo.ehviewer.dailycheck.today
|
||||
import com.hippo.ehviewer.util.AppConfig
|
||||
import com.hippo.ehviewer.util.ReadableTime
|
||||
import com.hippo.ehviewer.util.StatusCodeException
|
||||
import com.hippo.ehviewer.util.bodyAsUtf8Text
|
||||
import com.hippo.ehviewer.util.ensureSuccess
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import io.ktor.client.statement.HttpStatement
|
||||
import io.ktor.client.statement.bodyAsChannel
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.utils.io.pool.DirectByteBufferPool
|
||||
import io.ktor.utils.io.pool.useInstance
|
||||
import io.ktor.utils.io.readAvailable
|
||||
import java.io.File
|
||||
import java.nio.ByteBuffer
|
||||
import kotlin.math.ceil
|
||||
import kotlin.system.measureTimeMillis
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.serialization.SerializationException
|
||||
@@ -80,11 +87,9 @@ private const val MAX_REQUEST_SIZE = 25
|
||||
private const val MAX_SEQUENTIAL_REQUESTS = 5
|
||||
private const val REQUEST_INTERVAL = 5000L
|
||||
|
||||
private const val U_CONFIG_TEXT = "Selected Profile"
|
||||
|
||||
fun Either<String, ByteBuffer>.saveParseError(e: Throwable) {
|
||||
val dir = AppConfig.externalParseErrorDir ?: return
|
||||
val file = File(dir, ReadableTime.getFilenamableTime(System.currentTimeMillis()) + ".txt")
|
||||
val file = File(dir, ReadableTime.getFilenamableTime() + ".txt")
|
||||
file.sink().buffer().use { sink ->
|
||||
with(sink) {
|
||||
writeUtf8(e.message + "\n")
|
||||
@@ -94,7 +99,7 @@ fun Either<String, ByteBuffer>.saveParseError(e: Throwable) {
|
||||
}
|
||||
}
|
||||
|
||||
fun rethrowExactly(code: Int, body: Either<String, ByteBuffer>, e: Throwable): Nothing {
|
||||
fun rethrowExactly(status: HttpStatusCode, body: Either<String, ByteBuffer>, e: Throwable): Nothing {
|
||||
// Don't translate coroutine cancellation
|
||||
if (e is CancellationException) throw e
|
||||
|
||||
@@ -122,14 +127,12 @@ fun rethrowExactly(code: Int, body: Either<String, ByteBuffer>, e: Throwable): N
|
||||
}
|
||||
|
||||
// Check bad response code
|
||||
if (code >= 400) {
|
||||
throw StatusCodeException(code)
|
||||
}
|
||||
status.ensureSuccess()
|
||||
|
||||
if (e is ParseException || e is SerializationException) {
|
||||
body.onLeft { if ("<" !in it) throw EhException(it) }
|
||||
when (val message = e.cause?.message) {
|
||||
"No hits found!" -> throw EhException(R.string.gallery_list_empty_hit)
|
||||
"No hits found!" -> throw NoHitsFoundException()
|
||||
"No watched tags!" -> throw EhException(R.string.gallery_list_empty_hit_subscription)
|
||||
"Not logged in!" -> throw NotLoggedInException()
|
||||
is String -> if (message.startsWith("Your IP address")) throw EhException(message)
|
||||
@@ -149,7 +152,7 @@ suspend inline fun <T> HttpStatement.fetchUsingAsText(crossinline block: suspend
|
||||
runSuspendCatching {
|
||||
block(body)
|
||||
}.onFailure {
|
||||
rethrowExactly(response.status.value, body.left(), it)
|
||||
rethrowExactly(response.status, body.left(), it)
|
||||
}.getOrThrow()
|
||||
}
|
||||
|
||||
@@ -161,7 +164,7 @@ suspend inline fun <T> HttpStatement.fetchUsingAsByteBuffer(crossinline block: s
|
||||
block(buffer)
|
||||
}.onFailure {
|
||||
buffer.rewind()
|
||||
rethrowExactly(response.status.value, buffer.right(), it)
|
||||
rethrowExactly(response.status, buffer.right(), it)
|
||||
}.getOrThrow()
|
||||
}
|
||||
}
|
||||
@@ -177,16 +180,20 @@ object EhEngine {
|
||||
return ehRequest(url, referer).fetchUsingAsByteBuffer(TorrentParser::parse)
|
||||
}
|
||||
|
||||
suspend fun getArchiveList(url: String, gid: Long, token: String) = ehRequest(url, EhUrl.getGalleryDetailUrl(gid, token))
|
||||
.fetchUsingAsText(ArchiveParser::parse)
|
||||
.apply { funds = funds ?: ehRequest(EhUrl.URL_FUNDS).fetchUsingAsText(HomeParser::parseFunds) }
|
||||
suspend fun getArchiveList(url: String, gid: Long, token: String): ArchiveParser.Result {
|
||||
val funds = if (EhUtils.isExHentai) getFunds() else null
|
||||
return ehRequest(url, EhUrl.getGalleryDetailUrl(gid, token))
|
||||
.fetchUsingAsText(ArchiveParser::parse.partially2(funds))
|
||||
}
|
||||
|
||||
suspend fun getImageLimits() = parZip(
|
||||
{ ehRequest(EhUrl.URL_HOME).fetchUsingAsByteBuffer(HomeParser::parse) },
|
||||
{ ehRequest(EhUrl.URL_FUNDS).fetchUsingAsText(HomeParser::parseFunds) },
|
||||
{ getFunds() },
|
||||
{ limits, funds -> HomeParser.Result(limits, funds) },
|
||||
)
|
||||
|
||||
private suspend fun getFunds() = ehRequest(EhUrl.URL_FUNDS).fetchUsingAsText(HomeParser::parseFunds)
|
||||
|
||||
suspend fun getNews(parse: Boolean) = ehRequest(EhUrl.URL_NEWS, EhUrl.REFERER_E)
|
||||
.fetchUsingAsText { if (parse) EventPaneParser.parse(this) else null }
|
||||
|
||||
@@ -197,12 +204,12 @@ object EhEngine {
|
||||
|
||||
suspend fun getUConfig(url: String = EhUrl.uConfigUrl) {
|
||||
runSuspendCatching {
|
||||
ehRequest(url).fetchUsingAsText { check(U_CONFIG_TEXT in this) { "Unable to load config from $url!" } }
|
||||
ehRequest(url).fetchUsingAsByteBuffer(UserConfigParser::parse)
|
||||
}.onFailure { throwable ->
|
||||
// It may get redirected when accessing ex for the first time
|
||||
if (url == EhUrl.URL_UCONFIG_EX) {
|
||||
logcat(throwable)
|
||||
ehRequest(url).fetchUsingAsText { check(U_CONFIG_TEXT in this) { "Unable to load config from $url!" } }
|
||||
ehRequest(url).fetchUsingAsByteBuffer(UserConfigParser::parse)
|
||||
} else {
|
||||
throw throwable
|
||||
}
|
||||
@@ -234,6 +241,22 @@ object EhEngine {
|
||||
suspend fun getFavorites(url: String) = ehRequest(url, EhUrl.referer).fetchUsingAsByteBuffer(FavoritesParser::parse)
|
||||
.apply { galleryInfoList.fillInfo(url) }
|
||||
|
||||
suspend fun signIn(username: String, password: String): String {
|
||||
val referer = "https://forums.e-hentai.org/index.php?act=Login&CODE=00"
|
||||
val url = EhUrl.API_SIGN_IN
|
||||
val origin = "https://forums.e-hentai.org"
|
||||
return ehRequest(url, referer, origin) {
|
||||
formBody {
|
||||
append("referer", referer)
|
||||
append("b", "")
|
||||
append("bt", "")
|
||||
append("UserName", username)
|
||||
append("PassWord", password)
|
||||
append("CookieDate", "1")
|
||||
}
|
||||
}.fetchUsingAsText(SignInParser::parse)
|
||||
}
|
||||
|
||||
suspend fun commentGallery(url: String, comment: String, id: Long = -1) = ehRequest(url, url, EhUrl.origin) {
|
||||
formBody {
|
||||
if (id == -1L) {
|
||||
@@ -275,6 +298,10 @@ object EhEngine {
|
||||
}.executeSafely { }
|
||||
}
|
||||
|
||||
suspend fun getFavoriteNote(gid: Long, token: String) =
|
||||
ehRequest(EhUrl.getAddFavorites(gid, token), EhUrl.getGalleryDetailUrl(gid, token))
|
||||
.fetchUsingAsText(FavoritesParser::parseNote)
|
||||
|
||||
suspend fun downloadArchive(gid: Long, token: String, or: String, res: String, isHAtH: Boolean): String? {
|
||||
val url = EhUrl.getDownloadArchive(gid, token, or)
|
||||
val referer = EhUrl.getGalleryDetailUrl(gid, token)
|
||||
@@ -307,7 +334,7 @@ object EhEngine {
|
||||
|
||||
suspend fun resetImageLimits() = ehRequest(EhUrl.URL_HOME) {
|
||||
formBody {
|
||||
append("reset_imagelimit", "Reset Limit")
|
||||
append("reset_imagelimit", "Reset Quota")
|
||||
}
|
||||
}.fetchUsingAsText(HomeParser::parseResetLimits)
|
||||
|
||||
@@ -431,13 +458,13 @@ object EhEngine {
|
||||
}
|
||||
|
||||
suspend fun addFavorites(galleryList: List<Pair<Long, String>>, dstCat: Int) {
|
||||
galleryList.chunked(MAX_SEQUENTIAL_REQUESTS).forEachIndexed { index, chunk ->
|
||||
if (index != 0) {
|
||||
delay(REQUEST_INTERVAL)
|
||||
}
|
||||
chunk.parMap { (gid, token) ->
|
||||
galleryList.forEach { (gid, token) ->
|
||||
// https://github.com/FooIbar/EhViewer/issues/1190
|
||||
// Workaround for duplicate items when sorting by favorited time
|
||||
val timeTaken = measureTimeMillis {
|
||||
modifyFavorites(gid, token, dstCat)
|
||||
}
|
||||
delay(1000 - timeTaken)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,10 +15,12 @@
|
||||
*/
|
||||
package com.hippo.ehviewer.client
|
||||
|
||||
import com.hippo.ehviewer.BuildConfig
|
||||
import com.hippo.ehviewer.EhApplication.Companion.ktorClient
|
||||
import com.hippo.ehviewer.EhApplication.Companion.noRedirectKtorClient
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.util.bodyAsUtf8Text
|
||||
import com.hippo.ehviewer.util.ensureSuccess
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import io.ktor.client.request.HttpRequestBuilder
|
||||
import io.ktor.client.request.forms.FormDataContent
|
||||
@@ -44,13 +46,18 @@ import okio.BufferedSource
|
||||
|
||||
inline fun JsonObjectBuilder.array(name: String, builder: JsonArrayBuilder.() -> Unit) = put(name, buildJsonArray(builder))
|
||||
|
||||
suspend inline fun <reified T> HttpStatement.executeAndParseAs() = executeSafely { it.bodyAsUtf8Text().parseAs<T>() }
|
||||
suspend inline fun <reified T> HttpStatement.executeAndParseAs() = executeSafely {
|
||||
it.status.ensureSuccess()
|
||||
it.bodyAsUtf8Text().parseAs<T>()
|
||||
}
|
||||
|
||||
inline fun <reified T> BufferedSource.parseAs(): T = json.decodeFromBufferedSource(this)
|
||||
inline fun <reified T> String.parseAs(): T = json.decodeFromString(this)
|
||||
|
||||
val json = Json { ignoreUnknownKeys = true }
|
||||
|
||||
const val CHROME_USER_AGENT = "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Mobile Safari/537.36"
|
||||
const val CHROME_USER_AGENT = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${BuildConfig.CHROME_VERSION}.0.0.0 Safari/537.36"
|
||||
const val CHROME_MOBILE_USER_AGENT = "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${BuildConfig.CHROME_VERSION}.0.0.0 Mobile Safari/537.36"
|
||||
const val CHROME_ACCEPT = "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9"
|
||||
const val CHROME_ACCEPT_LANGUAGE = "en-US,en;q=0.5"
|
||||
|
||||
|
||||
@@ -24,15 +24,13 @@ import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.client.data.TagNamespace
|
||||
import com.hippo.ehviewer.util.AppConfig
|
||||
import com.hippo.ehviewer.util.FileUtils
|
||||
import com.hippo.ehviewer.util.StatusCodeException
|
||||
import com.hippo.ehviewer.util.copyTo
|
||||
import com.hippo.unifile.asUniFile
|
||||
import com.hippo.unifile.sha1
|
||||
import com.hippo.ehviewer.util.ensureSuccess
|
||||
import com.hippo.ehviewer.util.sha1
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.request.prepareGet
|
||||
import io.ktor.client.statement.bodyAsChannel
|
||||
import io.ktor.http.isSuccess
|
||||
import java.io.File
|
||||
import java.nio.charset.StandardCharsets
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -45,6 +43,7 @@ import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import moe.tarsin.coroutines.runSuspendCatching
|
||||
import okio.BufferedSource
|
||||
import okio.Path.Companion.toOkioPath
|
||||
import okio.buffer
|
||||
import okio.source
|
||||
import splitties.init.appCtx
|
||||
@@ -63,9 +62,7 @@ object EhTagDatabase : CoroutineScope {
|
||||
|
||||
var initialized by mutableStateOf(false)
|
||||
|
||||
fun getTranslation(prefix: String? = NAMESPACE_PREFIX, tag: String?): String? {
|
||||
return tagGroups[prefix]?.get(tag)?.trim()?.ifEmpty { null }
|
||||
}
|
||||
fun getTranslation(prefix: String? = NAMESPACE_PREFIX, tag: String?): String? = tagGroups[prefix]?.get(tag)?.trim()?.ifEmpty { null }
|
||||
|
||||
private fun internalSuggestFlow(
|
||||
tags: Map<String, String>,
|
||||
@@ -131,33 +128,22 @@ object EhTagDatabase : CoroutineScope {
|
||||
private fun String.containsIgnoreSpace(other: String, ignoreCase: Boolean = true): Boolean =
|
||||
removeSpace().contains(other.removeSpace(), ignoreCase)
|
||||
|
||||
private fun getMetadata(context: Context): Array<String>? {
|
||||
return context.resources.getStringArray(R.array.tag_translation_metadata)
|
||||
.takeIf { it.size == 4 }
|
||||
}
|
||||
private fun getMetadata(context: Context): Array<String>? = context.resources.getStringArray(R.array.tag_translation_metadata)
|
||||
.takeIf { it.size == 4 }
|
||||
|
||||
fun isTranslatable(context: Context): Boolean {
|
||||
return context.resources.getBoolean(R.bool.tag_translatable)
|
||||
}
|
||||
fun isTranslatable(context: Context): Boolean = context.resources.getBoolean(R.bool.tag_translatable)
|
||||
|
||||
private fun getFileContent(file: File): String? {
|
||||
return runCatching {
|
||||
file.source().buffer().use { it.readString(StandardCharsets.UTF_8) }
|
||||
}.getOrNull()
|
||||
}
|
||||
private fun getFileContent(file: File): String? = runCatching {
|
||||
file.source().buffer().use { it.readString(StandardCharsets.UTF_8) }
|
||||
}.getOrNull()
|
||||
|
||||
private fun checkData(sha1: String?, data: File): Boolean {
|
||||
return sha1 != null && sha1 == data.asUniFile().sha1()
|
||||
}
|
||||
private fun checkData(sha1: String?, data: File): Boolean = sha1 != null && sha1 == data.toOkioPath().sha1()
|
||||
|
||||
private suspend fun save(client: HttpClient, url: String, file: File) {
|
||||
runCatching {
|
||||
client.prepareGet(url).executeSafely {
|
||||
if (it.status.isSuccess()) {
|
||||
it.bodyAsChannel().copyTo(file)
|
||||
} else {
|
||||
throw StatusCodeException(it.status.value)
|
||||
}
|
||||
it.status.ensureSuccess()
|
||||
it.bodyAsChannel().copyTo(file)
|
||||
}
|
||||
}.onFailure {
|
||||
file.delete()
|
||||
|
||||
@@ -36,6 +36,7 @@ object EhUrl {
|
||||
const val API_E = "https://api.e-hentai.org/api.php"
|
||||
const val URL_UCONFIG_E = HOST_E + "uconfig.php"
|
||||
const val URL_MY_TAGS_E = HOST_E + "mytags"
|
||||
const val API_SIGN_IN = "https://forums.e-hentai.org/index.php?act=Login&CODE=01"
|
||||
const val URL_POPULAR_E = "https://e-hentai.org/popular"
|
||||
const val URL_POPULAR_EX = "https://exhentai.org/popular"
|
||||
const val URL_IMAGE_SEARCH_E = "https://upload.e-hentai.org/image_lookup.php"
|
||||
@@ -59,14 +60,10 @@ object EhUrl {
|
||||
}
|
||||
|
||||
val apiUrl: String
|
||||
get() = if (Settings.forceEhThumb) {
|
||||
API_E
|
||||
} else {
|
||||
when (Settings.gallerySite) {
|
||||
SITE_E -> API_E
|
||||
SITE_EX -> API_EX
|
||||
else -> API_E
|
||||
}
|
||||
get() = when (Settings.gallerySite) {
|
||||
SITE_E -> API_E
|
||||
SITE_EX -> API_EX
|
||||
else -> API_E
|
||||
}
|
||||
|
||||
val referer: String
|
||||
|
||||
@@ -88,21 +88,18 @@ object EhUtils {
|
||||
|
||||
fun getCategory(type: String?): Int {
|
||||
for (entry in CATEGORY_STRINGS) {
|
||||
for (str in entry.first)
|
||||
for (str in entry.first) {
|
||||
if (str.equals(type, ignoreCase = true)) {
|
||||
return entry.second
|
||||
}
|
||||
}
|
||||
}
|
||||
return UNKNOWN
|
||||
}
|
||||
|
||||
fun getCategory(type: Int): String {
|
||||
return CATEGORY_VALUES.getOrDefault(type, CATEGORY_VALUES[UNKNOWN])!![0]
|
||||
}
|
||||
fun getCategory(type: Int): String = CATEGORY_VALUES.getOrDefault(type, CATEGORY_VALUES[UNKNOWN])!![0]
|
||||
|
||||
private fun differenceDegrees(a: Float, b: Float): Float {
|
||||
return 180.0f - abs(abs(a - b) - 180.0f)
|
||||
}
|
||||
private fun differenceDegrees(a: Float, b: Float): Float = 180.0f - abs(abs(a - b) - 180.0f)
|
||||
|
||||
private fun sanitizeDegreesDouble(degrees: Float): Float {
|
||||
val deg = degrees % 360.0f
|
||||
@@ -152,20 +149,18 @@ object EhUtils {
|
||||
val categoryTextColor = Color(0xffe6e0e9)
|
||||
|
||||
fun signOut() {
|
||||
EhCookieStore.signOut()
|
||||
Settings.avatar = null
|
||||
Settings.displayName = null
|
||||
EhCookieStore.removeAllCookies()
|
||||
Settings.displayName.value = null
|
||||
Settings.hasSignedIn.value = false
|
||||
Settings.gallerySite = EhUrl.SITE_E
|
||||
Settings.needSignIn = true
|
||||
}
|
||||
|
||||
fun getSuitableTitle(gi: GalleryInfo): String {
|
||||
return if (Settings.showJpnTitle) {
|
||||
if (gi.titleJpn.isNullOrEmpty()) gi.title else gi.titleJpn
|
||||
} else {
|
||||
if (gi.title.isNullOrEmpty()) gi.titleJpn else gi.title
|
||||
}.orEmpty()
|
||||
}
|
||||
fun getSuitableTitle(gi: GalleryInfo): String = if (Settings.showJpnTitle) {
|
||||
if (gi.titleJpn.isNullOrEmpty()) gi.title else gi.titleJpn
|
||||
} else {
|
||||
if (gi.title.isNullOrEmpty()) gi.titleJpn else gi.title
|
||||
}.orEmpty()
|
||||
|
||||
fun extractTitle(fullTitle: String?): String? {
|
||||
var title: String = fullTitle ?: return null
|
||||
@@ -177,10 +172,7 @@ object EhUtils {
|
||||
return title.substringBeforeLast('|').trim().ifEmpty { null }
|
||||
}
|
||||
|
||||
fun handleThumbUrlResolution(url: String?): String? {
|
||||
if (null == url) {
|
||||
return null
|
||||
}
|
||||
fun handleThumbUrlResolution(url: String): String {
|
||||
val resolution = when (Settings.thumbResolution) {
|
||||
0 -> return url
|
||||
1 -> "250"
|
||||
|
||||
@@ -88,7 +88,8 @@ data class BaseGalleryInfo(
|
||||
|
||||
@Ignore
|
||||
override var favoriteNote: String? = null,
|
||||
) : GalleryInfo, Parcelable {
|
||||
) : GalleryInfo,
|
||||
Parcelable {
|
||||
constructor() : this(0, "")
|
||||
}
|
||||
|
||||
|
||||
@@ -55,7 +55,8 @@ data class GalleryDetail(
|
||||
var comments: GalleryCommentList,
|
||||
val previewPages: Int,
|
||||
val previewList: List<GalleryPreview>,
|
||||
) : GalleryInfo by galleryInfo, Parcelable {
|
||||
) : GalleryInfo by galleryInfo,
|
||||
Parcelable {
|
||||
fun fillInfo() {
|
||||
val index = LANGUAGES.indexOf(language)
|
||||
if (index != -1) simpleLanguage = S_LANGS[index]
|
||||
|
||||
@@ -134,14 +134,13 @@ interface GalleryInfo {
|
||||
}
|
||||
}
|
||||
|
||||
fun GalleryInfo.findBaseInfo(): BaseGalleryInfo {
|
||||
return when (this) {
|
||||
is BaseGalleryInfo -> this
|
||||
is GalleryDetail -> galleryInfo
|
||||
else -> unreachable()
|
||||
}
|
||||
fun GalleryInfo.findBaseInfo(): BaseGalleryInfo = when (this) {
|
||||
is BaseGalleryInfo -> this
|
||||
is GalleryDetail -> galleryInfo
|
||||
else -> unreachable()
|
||||
}
|
||||
|
||||
fun GalleryInfo.asGalleryDetail(): GalleryDetail? {
|
||||
return this as? GalleryDetail
|
||||
}
|
||||
fun GalleryInfo.asGalleryDetail(): GalleryDetail? = this as? GalleryDetail
|
||||
|
||||
val GalleryInfo.hasAds
|
||||
get() = simpleTags?.any { "extraneous ads" in it } ?: false
|
||||
@@ -19,19 +19,19 @@ import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
interface GalleryPreview : Parcelable {
|
||||
val imageKey: String
|
||||
val url: String
|
||||
val position: Int
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
data class LargeGalleryPreview(
|
||||
override val imageKey: String,
|
||||
override val url: String,
|
||||
override val position: Int,
|
||||
) : GalleryPreview
|
||||
|
||||
@Parcelize
|
||||
data class NormalGalleryPreview(
|
||||
override val imageKey: String,
|
||||
override val url: String,
|
||||
override val position: Int,
|
||||
val offsetX: Int,
|
||||
val clipWidth: Int,
|
||||
|
||||
@@ -22,4 +22,5 @@ import kotlinx.parcelize.Parcelize
|
||||
data class GalleryTagGroup(
|
||||
val groupName: String,
|
||||
private val tagList: MutableList<String> = mutableListOf(),
|
||||
) : Parcelable, MutableList<String> by tagList
|
||||
) : Parcelable,
|
||||
MutableList<String> by tagList
|
||||
@@ -42,6 +42,7 @@ data class ListUrlBuilder(
|
||||
var category: Int = EhUtils.NONE,
|
||||
private var mKeyword: String? = null,
|
||||
var hash: String? = null,
|
||||
var language: Int = -1,
|
||||
var advanceSearch: Int = -1,
|
||||
var minRating: Int = -1,
|
||||
var pageFrom: Int = -1,
|
||||
@@ -83,18 +84,16 @@ data class ListUrlBuilder(
|
||||
mNext = q.name.substringAfterLast('@', "").ifEmpty { null }
|
||||
}
|
||||
|
||||
fun toQuickSearch(name: String): QuickSearch {
|
||||
return QuickSearch(
|
||||
name = name,
|
||||
mode = mode,
|
||||
category = category,
|
||||
keyword = mKeyword,
|
||||
advanceSearch = advanceSearch,
|
||||
minRating = minRating,
|
||||
pageFrom = pageFrom,
|
||||
pageTo = pageTo,
|
||||
)
|
||||
}
|
||||
fun toQuickSearch(name: String): QuickSearch = QuickSearch(
|
||||
name = name,
|
||||
mode = mode,
|
||||
category = category,
|
||||
keyword = mKeyword,
|
||||
advanceSearch = advanceSearch,
|
||||
minRating = minRating,
|
||||
pageFrom = pageFrom,
|
||||
pageTo = pageTo,
|
||||
)
|
||||
|
||||
fun equalsQuickSearch(q: QuickSearch?): Boolean {
|
||||
if (null == q) {
|
||||
@@ -259,84 +258,89 @@ data class ListUrlBuilder(
|
||||
}
|
||||
}
|
||||
|
||||
fun build(): String {
|
||||
return when (mode) {
|
||||
MODE_NORMAL, MODE_SUBSCRIPTION -> ehUrl(EhUrl.WATCHED_PATH.takeIf { mode == MODE_SUBSCRIPTION }) {
|
||||
if (category > 0) {
|
||||
addQueryParameter("f_cats", (category.inv() and EhUtils.ALL_CATEGORY).toString())
|
||||
fun build(): String = when (mode) {
|
||||
MODE_NORMAL, MODE_SUBSCRIPTION -> ehUrl(EhUrl.WATCHED_PATH.takeIf { mode == MODE_SUBSCRIPTION }) {
|
||||
val category = if (!Settings.hasSignedIn.value && category <= 0) EhUtils.NON_H else category
|
||||
if (category > 0) {
|
||||
addQueryParameter("f_cats", (category.inv() and EhUtils.ALL_CATEGORY).toString())
|
||||
}
|
||||
val query = mKeyword?.let { keyword ->
|
||||
if (language == -1 || "gid:" in keyword || "l:" in keyword || "language:" in keyword) {
|
||||
keyword
|
||||
} else {
|
||||
val tag = GalleryInfo.S_LANG_TAGS[language]
|
||||
if (keyword.isNotEmpty()) {
|
||||
"$tag $keyword"
|
||||
} else {
|
||||
tag
|
||||
}
|
||||
}
|
||||
val query = mKeyword?.let {
|
||||
val index = Settings.languageFilter.value
|
||||
GalleryInfo.S_LANG_TAGS.getOrNull(index)?.let { lang ->
|
||||
"$it $lang"
|
||||
} ?: it
|
||||
}
|
||||
addQueryParameterIfNotBlank("f_search", query)
|
||||
addQueryParameterIfNotBlank("prev", mPrev)
|
||||
addQueryParameterIfNotBlank("next", mNext)
|
||||
addQueryParameterIfNotBlank("seek", mJumpTo)
|
||||
addQueryParameterIfNotBlank("range", mRange.takeIf { it > 0 }?.toString())
|
||||
// Advance search
|
||||
if (advanceSearch > 0 || minRating > 0 || pageFrom > 0 || pageTo > 0) {
|
||||
addQueryParameter("advsearch", "1")
|
||||
if (advanceSearch and AdvanceTable.SH != 0) {
|
||||
addQueryParameter("f_sh", "on")
|
||||
}
|
||||
addQueryParameterIfNotBlank("f_search", query)
|
||||
if (advanceSearch and AdvanceTable.STO != 0) {
|
||||
addQueryParameter("f_sto", "on")
|
||||
}
|
||||
if (advanceSearch and AdvanceTable.SFL != 0) {
|
||||
addQueryParameter("f_sfl", "on")
|
||||
}
|
||||
if (advanceSearch and AdvanceTable.SFU != 0) {
|
||||
addQueryParameter("f_sfu", "on")
|
||||
}
|
||||
if (advanceSearch and AdvanceTable.SFT != 0) {
|
||||
addQueryParameter("f_sft", "on")
|
||||
}
|
||||
// Set min star
|
||||
if (minRating > 0) {
|
||||
addQueryParameter("f_sr", "on")
|
||||
addQueryParameter("f_srdd", "$minRating")
|
||||
}
|
||||
// Pages
|
||||
if (pageFrom > 0 || pageTo > 0) {
|
||||
addQueryParameter("f_sp", "on")
|
||||
addQueryParameterIfNotBlank(
|
||||
"f_spf",
|
||||
pageFrom.takeIf { it > 0 }?.toString(),
|
||||
)
|
||||
addQueryParameterIfNotBlank(
|
||||
"f_spt",
|
||||
pageTo.takeIf { it > 0 }?.toString(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}.buildString()
|
||||
|
||||
MODE_UPLOADER, MODE_TAG -> {
|
||||
val path = if (mode == MODE_UPLOADER) "uploader" else "tag"
|
||||
ehUrl(listOf(path, requireNotNull(mKeyword))) {
|
||||
addQueryParameterIfNotBlank("prev", mPrev)
|
||||
addQueryParameterIfNotBlank("next", mNext)
|
||||
addQueryParameterIfNotBlank("seek", mJumpTo)
|
||||
addQueryParameterIfNotBlank("range", mRange.takeIf { it > 0 }?.toString())
|
||||
// Advance search
|
||||
if (advanceSearch > 0 || minRating > 0 || pageFrom > 0 || pageTo > 0) {
|
||||
addQueryParameter("advsearch", "1")
|
||||
if (advanceSearch and AdvanceTable.SH != 0) {
|
||||
addQueryParameter("f_sh", "on")
|
||||
}
|
||||
if (advanceSearch and AdvanceTable.STO != 0) {
|
||||
addQueryParameter("f_sto", "on")
|
||||
}
|
||||
if (advanceSearch and AdvanceTable.SFL != 0) {
|
||||
addQueryParameter("f_sfl", "on")
|
||||
}
|
||||
if (advanceSearch and AdvanceTable.SFU != 0) {
|
||||
addQueryParameter("f_sfu", "on")
|
||||
}
|
||||
if (advanceSearch and AdvanceTable.SFT != 0) {
|
||||
addQueryParameter("f_sft", "on")
|
||||
}
|
||||
// Set min star
|
||||
if (minRating > 0) {
|
||||
addQueryParameter("f_sr", "on")
|
||||
addQueryParameter("f_srdd", "$minRating")
|
||||
}
|
||||
// Pages
|
||||
if (pageFrom > 0 || pageTo > 0) {
|
||||
addQueryParameter("f_sp", "on")
|
||||
addQueryParameterIfNotBlank(
|
||||
"f_spf",
|
||||
pageFrom.takeIf { it > 0 }?.toString(),
|
||||
)
|
||||
addQueryParameterIfNotBlank(
|
||||
"f_spt",
|
||||
pageTo.takeIf { it > 0 }?.toString(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}.buildString()
|
||||
|
||||
MODE_UPLOADER, MODE_TAG -> {
|
||||
val path = if (mode == MODE_UPLOADER) "uploader" else "tag"
|
||||
ehUrl(listOf(path, requireNotNull(mKeyword))) {
|
||||
addQueryParameterIfNotBlank("prev", mPrev)
|
||||
addQueryParameterIfNotBlank("next", mNext)
|
||||
addQueryParameterIfNotBlank("seek", mJumpTo)
|
||||
addQueryParameterIfNotBlank("range", mRange.takeIf { it > 0 }?.toString())
|
||||
}.buildString()
|
||||
}
|
||||
|
||||
MODE_WHATS_HOT -> EhUrl.popularUrl
|
||||
MODE_IMAGE_SEARCH -> ehUrl {
|
||||
addQueryParameter("f_shash", requireNotNull(hash))
|
||||
}.buildString()
|
||||
MODE_TOPLIST -> {
|
||||
ehUrl("toplist.php", EhUrl.DOMAIN_E) {
|
||||
addQueryParameter("tl", requireNotNull(mKeyword))
|
||||
addQueryParameterIfNotBlank("p", mJumpTo)
|
||||
}.buildString()
|
||||
}
|
||||
|
||||
else -> throw IllegalStateException("Unexpected value: $mode")
|
||||
}
|
||||
|
||||
MODE_WHATS_HOT -> EhUrl.popularUrl
|
||||
MODE_IMAGE_SEARCH -> ehUrl {
|
||||
addQueryParameter("f_shash", requireNotNull(hash))
|
||||
}.buildString()
|
||||
MODE_TOPLIST -> {
|
||||
ehUrl("toplist.php", EhUrl.DOMAIN_E) {
|
||||
addQueryParameter("tl", requireNotNull(mKeyword))
|
||||
addQueryParameterIfNotBlank("p", mJumpTo)
|
||||
}.buildString()
|
||||
}
|
||||
|
||||
else -> throw IllegalStateException("Unexpected value: $mode")
|
||||
}
|
||||
|
||||
@IntDef(
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.hippo.ehviewer.client.exception
|
||||
|
||||
import com.hippo.ehviewer.R
|
||||
|
||||
class NoHitsFoundException : EhException(R.string.gallery_list_empty_hit)
|
||||
@@ -34,7 +34,7 @@ object ArchiveParser {
|
||||
private const val ERROR_INSUFFICIENT_FUNDS =
|
||||
"You do not have enough funds to download this archive."
|
||||
|
||||
fun parse(body: String): Result {
|
||||
fun parse(body: String, maybeFunds: Funds?): Result {
|
||||
val m = PATTERN_HATH_FORM.find(body)!!
|
||||
val paramOr = m.groupValues[1]
|
||||
val archiveList = ArrayList<Archive>()
|
||||
@@ -59,14 +59,12 @@ object ArchiveParser {
|
||||
val item = Archive(res, name, size, cost, true)
|
||||
archiveList.add(item)
|
||||
}
|
||||
val result = Result(paramOr, archiveList, null)
|
||||
PATTERN_CURRENT_FUNDS.find(body)?.groupValues?.run {
|
||||
val funds = maybeFunds ?: PATTERN_CURRENT_FUNDS.find(body)!!.groupValues.run {
|
||||
val fundsGP = ParserUtils.parseInt(get(1), 0)
|
||||
val fundsC = ParserUtils.parseInt(get(2), 0)
|
||||
val funds = HomeParser.Funds(fundsGP, fundsC)
|
||||
result.funds = funds
|
||||
Funds("%,d".format(fundsGP), "%,d".format(fundsC))
|
||||
}
|
||||
return result
|
||||
return Result(paramOr, archiveList, funds)
|
||||
}
|
||||
|
||||
fun parseArchiveUrl(body: String): String? {
|
||||
@@ -80,13 +78,13 @@ object ArchiveParser {
|
||||
// TODO: Check more errors
|
||||
}
|
||||
|
||||
class Archive(
|
||||
val res: String,
|
||||
val name: String,
|
||||
val size: String,
|
||||
val cost: String,
|
||||
val isHAtH: Boolean,
|
||||
)
|
||||
|
||||
class Result(val paramOr: String, val archiveList: List<Archive>, var funds: HomeParser.Funds?)
|
||||
data class Result(val paramOr: String, val archiveList: List<Archive>, val funds: Funds)
|
||||
}
|
||||
|
||||
class Archive(
|
||||
val res: String,
|
||||
val name: String,
|
||||
val size: String,
|
||||
val cost: String,
|
||||
val isHAtH: Boolean,
|
||||
)
|
||||
@@ -20,7 +20,5 @@ package com.hippo.ehviewer.client.parser
|
||||
import org.jsoup.Jsoup
|
||||
|
||||
object EventPaneParser {
|
||||
fun parse(body: String): String? {
|
||||
return Jsoup.parse(body).getElementById("eventpane")?.html()
|
||||
}
|
||||
fun parse(body: String): String? = Jsoup.parse(body).getElementById("eventpane")?.html()
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.hippo.ehviewer.client.parser
|
||||
|
||||
import com.hippo.ehviewer.client.exception.ParseException
|
||||
import com.hippo.ehviewer.util.unescapeXml
|
||||
import java.nio.ByteBuffer
|
||||
import kotlinx.serialization.Serializable
|
||||
import moe.tarsin.coroutines.runSuspendCatching
|
||||
@@ -17,13 +18,16 @@ data class FavParserResult(
|
||||
}
|
||||
|
||||
object FavoritesParser {
|
||||
fun parse(body: ByteBuffer): FavParserResult {
|
||||
return runSuspendCatching {
|
||||
unmarshalParsingAs<FavParserResult>(body, ::parseFav)
|
||||
}.getOrElse {
|
||||
throw ParseException("Parse favorites error", it)
|
||||
}
|
||||
private val NoteRegex = Regex("<textarea[^>]*>([^<]*)</textarea>")
|
||||
|
||||
fun parse(body: ByteBuffer): FavParserResult = runSuspendCatching {
|
||||
unmarshalParsingAs<FavParserResult>(body, ::parseFav)
|
||||
}.getOrElse {
|
||||
throw ParseException("Parse favorites error", it)
|
||||
}
|
||||
|
||||
fun parseNote(body: String) = NoteRegex.find(body)?.run { groupValues[1].unescapeXml() }
|
||||
?: throw ParseException("Can't parse favorite note")
|
||||
}
|
||||
|
||||
private external fun parseFav(body: ByteBuffer, limit: Int = body.limit()): Int
|
||||
@@ -21,15 +21,13 @@ import com.hippo.ehviewer.util.ExceptionUtils
|
||||
import org.jsoup.Jsoup
|
||||
|
||||
object ForumsParser {
|
||||
fun parse(body: String): String {
|
||||
return runCatching {
|
||||
val d = Jsoup.parse(body, EhUrl.URL_FORUMS)
|
||||
val userlinks = d.getElementById("userlinks")
|
||||
val child = userlinks!!.child(0).child(0).child(0)
|
||||
child.attr("href")
|
||||
}.getOrElse {
|
||||
ExceptionUtils.throwIfFatal(it)
|
||||
throw NotLoggedInException()
|
||||
}
|
||||
fun parse(body: String): String = runCatching {
|
||||
val d = Jsoup.parse(body, EhUrl.URL_FORUMS)
|
||||
val userlinks = d.getElementById("userlinks")
|
||||
val child = userlinks!!.child(0).child(0).child(0)
|
||||
child.attr("href")
|
||||
}.getOrElse {
|
||||
ExceptionUtils.throwIfFatal(it)
|
||||
throw NotLoggedInException()
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,6 @@
|
||||
package com.hippo.ehviewer.client.parser
|
||||
|
||||
import com.hippo.ehviewer.client.EhUtils.getCategory
|
||||
import com.hippo.ehviewer.client.EhUtils.handleThumbUrlResolution
|
||||
import com.hippo.ehviewer.client.data.GalleryInfo
|
||||
import com.hippo.ehviewer.client.getThumbKey
|
||||
import com.hippo.ehviewer.client.parseAs
|
||||
@@ -32,7 +31,7 @@ object GalleryApiParser {
|
||||
title = item.title.unescapeXml()
|
||||
titleJpn = item.titleJpn.unescapeXml()
|
||||
category = getCategory(item.category)
|
||||
thumbKey = getThumbKey(handleThumbUrlResolution(item.thumb)!!)
|
||||
thumbKey = getThumbKey(item.thumb)
|
||||
uploader = item.uploader?.unescapeXml()
|
||||
posted = ParserUtils.formatDate(item.posted * 1000)
|
||||
rating = item.rating
|
||||
|
||||
@@ -21,7 +21,6 @@ import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.client.EhFilter
|
||||
import com.hippo.ehviewer.client.EhUtils
|
||||
import com.hippo.ehviewer.client.EhUtils.getCategory
|
||||
import com.hippo.ehviewer.client.EhUtils.handleThumbUrlResolution
|
||||
import com.hippo.ehviewer.client.data.BaseGalleryInfo
|
||||
import com.hippo.ehviewer.client.data.GalleryComment
|
||||
import com.hippo.ehviewer.client.data.GalleryCommentList
|
||||
@@ -62,7 +61,7 @@ object GalleryDetailParser {
|
||||
RegexOption.DOT_MATCHES_ALL,
|
||||
)
|
||||
private val PATTERN_TORRENT =
|
||||
Regex("<a[^<>]*onclick=\"return popUp\\('([^']+)'[^)]+\\)\">Torrent Download[^<]+(\\d+)[^<]+</a")
|
||||
Regex("<a[^<>]*onclick=\"return popUp\\('([^']+)'[^)]+\\)\">Torrent Download \\((\\d+)\\)</a>")
|
||||
private val PATTERN_ARCHIVE =
|
||||
Regex("<a[^<>]*onclick=\"return popUp\\('([^']+)'[^)]+\\)\">Archive Download</a>")
|
||||
private val PATTERN_COVER = Regex("width:(\\d+)px; height:(\\d+)px.+?url\\((.+?)\\)")
|
||||
@@ -140,11 +139,7 @@ object GalleryDetailParser {
|
||||
val gm = d.getElementsByClass("gm")[0]
|
||||
// Thumb url
|
||||
gm.getElementById("gd1")?.child(0)?.attr("style")?.trim()?.let {
|
||||
gd.thumbKey = getThumbKey(
|
||||
PATTERN_COVER.find(it)?.run {
|
||||
handleThumbUrlResolution(groupValues[3])
|
||||
}!!,
|
||||
)
|
||||
gd.thumbKey = getThumbKey(PATTERN_COVER.find(it)!!.groupValues[3])
|
||||
}
|
||||
|
||||
gd.title = gm.getElementById("gn")?.text()?.trim()
|
||||
@@ -283,56 +278,50 @@ object GalleryDetailParser {
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseTagGroup(element: Element): GalleryTagGroup? {
|
||||
return try {
|
||||
var nameSpace = element.child(0).text()
|
||||
// Remove last ':'
|
||||
nameSpace = nameSpace.substring(0, nameSpace.length - 1)
|
||||
val group = GalleryTagGroup(nameSpace)
|
||||
val tags = element.child(1).children()
|
||||
tags.forEach {
|
||||
var tag = it.text()
|
||||
// Sometimes parody tag is followed with '|' and english translate, just remove them
|
||||
val index = tag.indexOf('|')
|
||||
if (index >= 0) {
|
||||
tag = tag.substring(0, index).trim()
|
||||
}
|
||||
if (it.className() == "gtw") {
|
||||
tag = "_$tag" // weak tag
|
||||
}
|
||||
group.add(tag)
|
||||
private fun parseTagGroup(element: Element): GalleryTagGroup? = try {
|
||||
var nameSpace = element.child(0).text()
|
||||
// Remove last ':'
|
||||
nameSpace = nameSpace.substring(0, nameSpace.length - 1)
|
||||
val group = GalleryTagGroup(nameSpace)
|
||||
val tags = element.child(1).children()
|
||||
tags.forEach {
|
||||
var tag = it.text()
|
||||
// Sometimes parody tag is followed with '|' and english translate, just remove them
|
||||
val index = tag.indexOf('|')
|
||||
if (index >= 0) {
|
||||
tag = tag.substring(0, index).trim()
|
||||
}
|
||||
if (group.size > 0) group else null
|
||||
} catch (e: Throwable) {
|
||||
ExceptionUtils.throwIfFatal(e)
|
||||
logcat(e)
|
||||
null
|
||||
if (it.className() == "gtw") {
|
||||
tag = "_$tag" // weak tag
|
||||
}
|
||||
group.add(tag)
|
||||
}
|
||||
if (group.size > 0) group else null
|
||||
} catch (e: Throwable) {
|
||||
ExceptionUtils.throwIfFatal(e)
|
||||
logcat(e)
|
||||
null
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse tag groups with html parser
|
||||
*/
|
||||
private fun parseTagGroups(document: Document): List<GalleryTagGroup> {
|
||||
return try {
|
||||
val taglist = document.getElementById("taglist")!!
|
||||
val tagGroups = taglist.child(0).child(0).children()
|
||||
parseTagGroups(tagGroups)
|
||||
} catch (e: Throwable) {
|
||||
ExceptionUtils.throwIfFatal(e)
|
||||
logcat(e)
|
||||
emptyList()
|
||||
}
|
||||
private fun parseTagGroups(document: Document): List<GalleryTagGroup> = try {
|
||||
val taglist = document.getElementById("taglist")!!
|
||||
val tagGroups = taglist.child(0).child(0).children()
|
||||
parseTagGroups(tagGroups)
|
||||
} catch (e: Throwable) {
|
||||
ExceptionUtils.throwIfFatal(e)
|
||||
logcat(e)
|
||||
emptyList()
|
||||
}
|
||||
|
||||
private fun parseTagGroups(trs: Elements): List<GalleryTagGroup> {
|
||||
return try {
|
||||
trs.mapNotNull { parseTagGroup(it) }
|
||||
} catch (e: Throwable) {
|
||||
ExceptionUtils.throwIfFatal(e)
|
||||
logcat(e)
|
||||
emptyList()
|
||||
}
|
||||
private fun parseTagGroups(trs: Elements): List<GalleryTagGroup> = try {
|
||||
trs.mapNotNull { parseTagGroup(it) }
|
||||
} catch (e: Throwable) {
|
||||
ExceptionUtils.throwIfFatal(e)
|
||||
logcat(e)
|
||||
emptyList()
|
||||
}
|
||||
|
||||
private suspend fun parseComment(element: Element): GalleryComment? {
|
||||
@@ -449,30 +438,24 @@ object GalleryDetailParser {
|
||||
/**
|
||||
* Parse preview pages with regular expressions
|
||||
*/
|
||||
fun parsePreviewPages(body: String): Int {
|
||||
return PATTERN_PREVIEW_PAGES.find(body)?.groupValues?.get(1)?.toIntOrNull()
|
||||
?: throw ParseException("Parse preview page count error")
|
||||
}
|
||||
fun parsePreviewPages(body: String): Int = PATTERN_PREVIEW_PAGES.find(body)?.groupValues?.get(1)?.toIntOrNull()
|
||||
?: throw ParseException("Parse preview page count error")
|
||||
|
||||
/**
|
||||
* Parse pages with regular expressions
|
||||
*/
|
||||
fun parsePages(body: String): Int {
|
||||
return PATTERN_PAGES.find(body)?.groupValues?.get(1)?.toIntOrNull()
|
||||
?: throw ParseException("Parse pages error")
|
||||
}
|
||||
fun parsePages(body: String): Int = PATTERN_PAGES.find(body)?.groupValues?.get(1)?.toIntOrNull()
|
||||
?: throw ParseException("Parse pages error")
|
||||
|
||||
fun parsePreviewList(body: String): Pair<List<GalleryPreview>, List<String>> {
|
||||
return runCatching { parseNormalPreview(body) }.getOrElse { parseLargePreview(body) }
|
||||
}
|
||||
fun parsePreviewList(body: String): Pair<List<GalleryPreview>, List<String>> = runCatching { parseNormalPreview(body) }.getOrElse { parseLargePreview(body) }
|
||||
|
||||
private fun parseLargePreview(body: String): Pair<List<GalleryPreview>, List<String>> {
|
||||
check(PATTERN_LARGE_PREVIEW.containsMatchIn(body))
|
||||
return PATTERN_LARGE_PREVIEW.findAll(body).unzip {
|
||||
val position = it.groupValues[2].toInt() - 1
|
||||
val imageKey = getThumbKey(it.groupValues[3].trim())
|
||||
val url = it.groupValues[3].trim()
|
||||
val pageUrl = it.groupValues[1].trim()
|
||||
LargeGalleryPreview(imageKey, position) to pageUrl
|
||||
LargeGalleryPreview(url, position) to pageUrl
|
||||
}.run {
|
||||
first.toList() to second.toList()
|
||||
}
|
||||
@@ -482,12 +465,12 @@ object GalleryDetailParser {
|
||||
check(PATTERN_NORMAL_PREVIEW.containsMatchIn(body))
|
||||
return PATTERN_NORMAL_PREVIEW.findAll(body).unzip {
|
||||
val position = it.groupValues[6].toInt() - 1
|
||||
val imageKey = getThumbKey(it.groupValues[3].trim())
|
||||
val url = it.groupValues[3].trim()
|
||||
val xOffset = it.groupValues[4].toIntOrNull() ?: 0
|
||||
val width = it.groupValues[1].toInt()
|
||||
val height = it.groupValues[2].toInt()
|
||||
val pageUrl = it.groupValues[5].trim()
|
||||
NormalGalleryPreview(imageKey, position, xOffset, width, height) to pageUrl
|
||||
NormalGalleryPreview(url, position, xOffset, width, height) to pageUrl
|
||||
}.run {
|
||||
first.toList() to second.toList()
|
||||
}
|
||||
|
||||
+4
-8
@@ -19,22 +19,18 @@ package com.hippo.ehviewer.client.parser
|
||||
|
||||
import com.hippo.ehviewer.client.exception.ParseException
|
||||
import com.hippo.ehviewer.client.parseAs
|
||||
import com.hippo.ehviewer.util.ExceptionUtils
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
object GalleryMultiPageViewerPTokenParser {
|
||||
private const val IMAGE_LIST_STRING = "var imagelist = "
|
||||
|
||||
fun parse(body: String): List<String> {
|
||||
fun parse(body: String): List<String> = runCatching {
|
||||
val index = body.indexOf(IMAGE_LIST_STRING)
|
||||
val imagelist = body.substring(index + IMAGE_LIST_STRING.length, body.indexOf(";", index))
|
||||
return runCatching {
|
||||
imagelist.parseAs<List<Item>>().map(Item::token)
|
||||
}.getOrElse {
|
||||
ExceptionUtils.throwIfFatal(it)
|
||||
throw ParseException("Parse pToken from MPV error", it)
|
||||
}
|
||||
imagelist.parseAs<List<Item>>().map(Item::token)
|
||||
}.getOrElse {
|
||||
throw ParseException("Parse pToken from MPV error", it)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
|
||||
@@ -22,14 +22,12 @@ import eu.kanade.tachiyomi.util.system.logcat
|
||||
import org.jsoup.Jsoup
|
||||
|
||||
object GalleryNotAvailableParser {
|
||||
fun parse(body: String): String? {
|
||||
return runCatching {
|
||||
val e = Jsoup.parse(body).getElementsByClass("d").first()
|
||||
e!!.child(0).html().replace("<br>", "\n")
|
||||
}.getOrElse {
|
||||
ExceptionUtils.throwIfFatal(it)
|
||||
logcat(it)
|
||||
null
|
||||
}
|
||||
fun parse(body: String): String? = runCatching {
|
||||
val e = Jsoup.parse(body).getElementsByClass("d").first()
|
||||
e!!.child(0).html().replace("<br>", "\n")
|
||||
}.getOrElse {
|
||||
ExceptionUtils.throwIfFatal(it)
|
||||
logcat(it)
|
||||
null
|
||||
}
|
||||
}
|
||||
@@ -32,13 +32,11 @@ object GalleryTokenApiParser {
|
||||
* ]
|
||||
* }
|
||||
*/
|
||||
fun parse(body: String): String {
|
||||
return runCatching {
|
||||
body.parseAs<Result>().tokenList[0].token
|
||||
}.getOrElse {
|
||||
ExceptionUtils.throwIfFatal(it)
|
||||
throw EhException(body.parseAs<Error>().error)
|
||||
}
|
||||
fun parse(body: String): String = runCatching {
|
||||
body.parseAs<Result>().tokenList[0].token
|
||||
}.getOrElse {
|
||||
ExceptionUtils.throwIfFatal(it)
|
||||
throw EhException(body.parseAs<Error>().error)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
|
||||
@@ -25,18 +25,18 @@ object HomeParser {
|
||||
PATTERN_FUNDS.find(body)?.groupValues?.run {
|
||||
val fundsC = ParserUtils.parseInt(get(1), 0)
|
||||
val fundsGP = ParserUtils.parseInt(get(2), 0) * 1000
|
||||
return Funds(fundsGP, fundsC)
|
||||
return Funds("%,d+".format(fundsGP), "%,d".format(fundsC))
|
||||
}
|
||||
throw ParseException("Parse funds error")
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
data class Funds(val fundsGP: Int, val fundsC: Int) : Parcelable
|
||||
|
||||
@Parcelize
|
||||
data class Result(val limits: Limits, val funds: Funds) : Parcelable
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
data class Funds(val gp: String, val credit: String) : Parcelable
|
||||
|
||||
@Parcelize
|
||||
@Serializable
|
||||
data class Limits(val current: Int, val maximum: Int, val resetCost: Int) : Parcelable
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
package com.hippo.ehviewer.client.parser
|
||||
|
||||
import com.hippo.ehviewer.util.toEpochMillis
|
||||
import com.hippo.ehviewer.util.toIntOrDefault
|
||||
import com.hippo.ehviewer.util.toLocalDateTime
|
||||
import com.hippo.ehviewer.util.toLongOrDefault
|
||||
@@ -36,20 +37,13 @@ object ParserUtils {
|
||||
minute()
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun formatDate(time: Long): String {
|
||||
return formatter.format(time.toLocalDateTime())
|
||||
}
|
||||
fun formatDate(time: Long): String = formatter.format(time.toLocalDateTime())
|
||||
|
||||
fun trim(str: String?): String {
|
||||
return str?.unescapeXml()?.trim() ?: ""
|
||||
}
|
||||
fun parseDate(str: String): Long = formatter.parse(str).toEpochMillis()
|
||||
|
||||
fun parseInt(str: String?, defValue: Int): Int {
|
||||
return trim(str).replace(",", "").toIntOrDefault(defValue)
|
||||
}
|
||||
fun trim(str: String?): String = str?.unescapeXml()?.trim() ?: ""
|
||||
|
||||
fun parseLong(str: String?, defValue: Long): Long {
|
||||
return trim(str).replace(",", "").toLongOrDefault(defValue)
|
||||
}
|
||||
fun parseInt(str: String?, defValue: Int): Int = trim(str).replace(",", "").toIntOrDefault(defValue)
|
||||
|
||||
fun parseLong(str: String?, defValue: Long): Long = trim(str).replace(",", "").toLongOrDefault(defValue)
|
||||
}
|
||||
@@ -22,31 +22,29 @@ import eu.kanade.tachiyomi.util.system.logcat
|
||||
import org.jsoup.Jsoup
|
||||
|
||||
object ProfileParser {
|
||||
fun parse(body: String): Result {
|
||||
return runCatching {
|
||||
val d = Jsoup.parse(body)
|
||||
val profilename = d.getElementById("profilename")
|
||||
val displayName = profilename!!.child(0).text()
|
||||
val avatar = runCatching {
|
||||
val avatar =
|
||||
profilename.nextElementSibling()!!.nextElementSibling()!!.child(0).attr("src")
|
||||
if (avatar.isEmpty()) {
|
||||
null
|
||||
} else if (!avatar.startsWith("http")) {
|
||||
EhUrl.URL_FORUMS + avatar
|
||||
} else {
|
||||
avatar
|
||||
}
|
||||
}.getOrElse {
|
||||
ExceptionUtils.throwIfFatal(it)
|
||||
logcat { "No avatar" }
|
||||
fun parse(body: String): Result = runCatching {
|
||||
val d = Jsoup.parse(body)
|
||||
val profilename = d.getElementById("profilename")
|
||||
val displayName = profilename!!.child(0).text()
|
||||
val avatar = runCatching {
|
||||
val avatar =
|
||||
profilename.nextElementSibling()!!.nextElementSibling()!!.child(0).attr("src")
|
||||
if (avatar.isEmpty()) {
|
||||
null
|
||||
} else if (!avatar.startsWith("http")) {
|
||||
EhUrl.URL_FORUMS + avatar
|
||||
} else {
|
||||
avatar
|
||||
}
|
||||
Result(displayName, avatar)
|
||||
}.getOrElse {
|
||||
ExceptionUtils.throwIfFatal(it)
|
||||
throw ParseException("Parse forums error")
|
||||
logcat { "No avatar" }
|
||||
null
|
||||
}
|
||||
Result(displayName, avatar)
|
||||
}.getOrElse {
|
||||
ExceptionUtils.throwIfFatal(it)
|
||||
throw ParseException("Parse forums error")
|
||||
}
|
||||
|
||||
class Result(val displayName: String?, val avatar: String?)
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright 2016 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.hippo.ehviewer.client.parser
|
||||
|
||||
import com.hippo.ehviewer.client.exception.EhException
|
||||
import com.hippo.ehviewer.client.exception.ParseException
|
||||
|
||||
object SignInParser {
|
||||
private val NAME_PATTERN = Regex("<p>You are now logged in as: (.+?)<")
|
||||
private val ERROR_PATTERN = Regex(
|
||||
"<h4>The error returned was:</h4>\\s*<p>(.+?)</p>" +
|
||||
"|<span class=\"postcolor\">(.+?)</span>",
|
||||
)
|
||||
|
||||
fun parse(body: String): String = NAME_PATTERN.find(body)?.let {
|
||||
it.groupValues[1]
|
||||
} ?: ERROR_PATTERN.find(body)?.let {
|
||||
throw EhException(it.groupValues[1].ifEmpty { it.groupValues[2] })
|
||||
} ?: throw ParseException("Can't parse sign in")
|
||||
}
|
||||
@@ -14,16 +14,18 @@ object TorrentParser {
|
||||
|
||||
@Serializable
|
||||
data class Torrent(
|
||||
val outdated: Boolean,
|
||||
val posted: String,
|
||||
val size: String,
|
||||
val seeds: Int,
|
||||
val peers: Int,
|
||||
val downloads: Int,
|
||||
val uploader: String,
|
||||
val url: String,
|
||||
val name: String,
|
||||
)
|
||||
|
||||
fun Torrent.format() = "[$posted] $name [$size] [↑$seeds ↓$peers ✓$downloads]"
|
||||
fun Torrent.format() = "↑$seeds ↓$peers ✓$downloads"
|
||||
|
||||
typealias TorrentResult = List<Torrent>
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.hippo.ehviewer.client.parser
|
||||
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.client.exception.ParseException
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
object UserConfigParser {
|
||||
private external fun parseFavCat(body: ByteBuffer, size: Int = body.limit()): Int
|
||||
|
||||
fun parse(body: ByteBuffer) = runCatching {
|
||||
val favCat = unmarshalParsingAs<Array<String>>(body, ::parseFavCat)
|
||||
Settings.favCat = favCat
|
||||
}.onFailure {
|
||||
throw ParseException("Can't parse user config", it)
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,5 @@ import com.hippo.ehviewer.client.parseAs
|
||||
|
||||
object VoteTagParser {
|
||||
// {"error":"The tag \"neko\" is not allowed. Use character:neko or artist:neko"}
|
||||
fun parse(body: String): String? {
|
||||
return runCatching { body.parseAs<Error>().error }.getOrNull()
|
||||
}
|
||||
fun parse(body: String): String? = runCatching { body.parseAs<Error>().error }.getOrNull()
|
||||
}
|
||||
@@ -1,88 +1,61 @@
|
||||
package com.hippo.ehviewer.coil
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import androidx.compose.ui.unit.IntRect
|
||||
import coil3.BitmapImage
|
||||
import coil3.Extras
|
||||
import coil3.Image
|
||||
import coil3.asCoilImage
|
||||
import coil3.getExtra
|
||||
import android.hardware.HardwareBuffer
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import androidx.compose.ui.unit.toIntRect
|
||||
import arrow.fx.coroutines.autoCloseable
|
||||
import arrow.fx.coroutines.resourceScope
|
||||
import coil3.asImage
|
||||
import coil3.intercept.Interceptor
|
||||
import coil3.intercept.Interceptor.Chain
|
||||
import coil3.request.ImageRequest
|
||||
import coil3.request.ImageResult
|
||||
import coil3.request.SuccessResult
|
||||
import coil3.request.allowHardware
|
||||
import com.hippo.ehviewer.image.detectBorder
|
||||
import com.hippo.ehviewer.util.isAtLeastO
|
||||
import com.hippo.ehviewer.image.copyBitmapToAHB
|
||||
import com.hippo.ehviewer.util.isAtLeastQ
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import moe.tarsin.coroutines.runSuspendCatching
|
||||
|
||||
private const val CROP_THRESHOLD = 0.75f
|
||||
private const val RATIO_THRESHOLD = 2
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
private const val FORMAT = HardwareBuffer.RGBA_8888
|
||||
|
||||
private val maybeCropBorderKey = Extras.Key(default = false)
|
||||
|
||||
fun ImageRequest.Builder.maybeCropBorder(enable: Boolean) = apply {
|
||||
extras[maybeCropBorderKey] = enable
|
||||
}
|
||||
|
||||
val ImageRequest.maybeCropBorder: Boolean
|
||||
get() = getExtra(maybeCropBorderKey)
|
||||
|
||||
class BitmapImageWithRect(
|
||||
val rect: IntRect,
|
||||
val image: Image,
|
||||
) : Image by image
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
private const val USAGE = HardwareBuffer.USAGE_CPU_WRITE_RARELY or HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE
|
||||
|
||||
object CropBorderInterceptor : Interceptor {
|
||||
override suspend fun intercept(chain: Chain): ImageResult {
|
||||
val req = chain.request
|
||||
if (req.maybeCropBorder) {
|
||||
val hw = req.allowHardware && isAtLeastO
|
||||
val result = if (hw) {
|
||||
val nonHw = req.newBuilder().allowHardware(false).build()
|
||||
chain.withRequest(nonHw).proceed()
|
||||
} else {
|
||||
chain.proceed()
|
||||
}
|
||||
if (result is SuccessResult) {
|
||||
val image = result.image
|
||||
if (image is BitmapImage) {
|
||||
val bitmap = image.bitmap
|
||||
val ratio = if (image.height > image.width) {
|
||||
image.height / image.width
|
||||
} else {
|
||||
image.width / image.height
|
||||
}
|
||||
val rect = if (ratio < RATIO_THRESHOLD) {
|
||||
val array = detectBorder(bitmap)
|
||||
val minWidth = image.width * CROP_THRESHOLD
|
||||
val minHeight = image.height * CROP_THRESHOLD
|
||||
IntRect(array[0], array[1], array[2], array[3]).takeIf {
|
||||
it.width > minWidth && it.height > minHeight
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
val maybeHwImage = if (hw) {
|
||||
// Upload to Graphical Buffer to accelerate render
|
||||
// May fail if the image is too long
|
||||
bitmap.copy(Bitmap.Config.HARDWARE, false)?.run {
|
||||
bitmap.recycle()
|
||||
asCoilImage()
|
||||
} ?: image
|
||||
} else {
|
||||
image
|
||||
}
|
||||
val new = if (rect != null) {
|
||||
BitmapImageWithRect(rect, maybeHwImage)
|
||||
} else {
|
||||
maybeHwImage
|
||||
}
|
||||
return result.copy(image = new, request = result.request, dataSource = result.dataSource)
|
||||
override suspend fun intercept(chain: Interceptor.Chain): ImageResult {
|
||||
val result = chain.proceed()
|
||||
if (result is SuccessResult) {
|
||||
val image = result.image
|
||||
if (image is BitmapImageWithExtraInfo) {
|
||||
// Copy with cropped region
|
||||
val srcSize = IntSize(image.width, image.height)
|
||||
if (image.rect.size != srcSize) {
|
||||
val (x, y) = image.rect.topLeft
|
||||
val (w, h) = image.rect.size
|
||||
val src = image.image.bitmap
|
||||
|
||||
// Large hardware bitmaps have rendering issues (e.g. crash, empty) on some devices.
|
||||
// This is not ideal but I haven't figured out how to probe the threshold.
|
||||
// All we know is that it's less than the maximum texture size.
|
||||
val meetHardwareThreshold = maxOf(w, h) <= chain.request.hardwareThreshold
|
||||
val bitmap = when {
|
||||
isAtLeastQ && meetHardwareThreshold -> runSuspendCatching {
|
||||
resourceScope {
|
||||
val buffer = autoCloseable { HardwareBuffer.create(w, h, FORMAT, 1, USAGE) }
|
||||
copyBitmapToAHB(src, buffer, x, y)
|
||||
Bitmap.wrapHardwareBuffer(buffer, src.colorSpace)
|
||||
}
|
||||
}.onFailure { logcat(it) }.getOrNull()
|
||||
else -> null
|
||||
} ?: Bitmap.createBitmap(src, x, y, w, h)
|
||||
|
||||
src.recycle()
|
||||
return result.copy(image = image.copy(image = bitmap.asImage(), rect = image.rect.size.toIntRect()))
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
return chain.proceed()
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.hippo.ehviewer.coil
|
||||
|
||||
import androidx.compose.ui.unit.IntRect
|
||||
import coil3.Extras
|
||||
import coil3.getExtra
|
||||
import coil3.intercept.Interceptor
|
||||
import coil3.intercept.Interceptor.Chain
|
||||
import coil3.request.ImageRequest
|
||||
import coil3.request.ImageResult
|
||||
import coil3.request.SuccessResult
|
||||
import com.hippo.ehviewer.image.detectBorder
|
||||
|
||||
private const val CROP_THRESHOLD = 0.75f
|
||||
private const val RATIO_THRESHOLD = 2
|
||||
|
||||
private val maybeCropBorderKey = Extras.Key(default = false)
|
||||
|
||||
fun ImageRequest.Builder.maybeCropBorder(enable: Boolean) = apply {
|
||||
extras[maybeCropBorderKey] = enable
|
||||
}
|
||||
|
||||
val ImageRequest.maybeCropBorder: Boolean
|
||||
get() = getExtra(maybeCropBorderKey)
|
||||
|
||||
object DetectBorderInterceptor : Interceptor {
|
||||
override suspend fun intercept(chain: Chain): ImageResult {
|
||||
val result = chain.proceed()
|
||||
if (chain.request.maybeCropBorder && result is SuccessResult) {
|
||||
val image = result.image
|
||||
if (image is BitmapImageWithExtraInfo && !image.hasQrCode) {
|
||||
val bitmap = image.image.bitmap
|
||||
val ratio = if (image.height > image.width) {
|
||||
image.height / image.width
|
||||
} else {
|
||||
image.width / image.height
|
||||
}
|
||||
if (ratio < RATIO_THRESHOLD) {
|
||||
val array = detectBorder(bitmap)
|
||||
val minWidth = image.width * CROP_THRESHOLD
|
||||
val minHeight = image.height * CROP_THRESHOLD
|
||||
val rect = IntRect(array[0], array[1], array[2], array[3])
|
||||
if (rect.width > minWidth && rect.height > minHeight) {
|
||||
val new = image.copy(rect = rect)
|
||||
return result.copy(image = new)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -7,43 +7,31 @@ import com.hippo.ehviewer.EhApplication.Companion.imageCache
|
||||
import com.hippo.ehviewer.download.downloadLocation
|
||||
import com.hippo.ehviewer.spider.DownloadInfoMagics.decodeMagicRequestOrUrl
|
||||
import com.hippo.ehviewer.util.sendTo
|
||||
import com.hippo.unifile.asUniFile
|
||||
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||
import com.hippo.files.isDirectory
|
||||
import com.hippo.files.isFile
|
||||
import com.hippo.files.toUri
|
||||
|
||||
object DownloadThumbInterceptor : Interceptor {
|
||||
// TODO: Remove this after a few releases
|
||||
private const val LEGACY_THUMB_FILE = ".thumb"
|
||||
private const val THUMB_FILE = "thumb.jpg"
|
||||
override suspend fun intercept(chain: Interceptor.Chain): ImageResult {
|
||||
val magicOrUrl = chain.request.data as? String
|
||||
if (magicOrUrl != null) {
|
||||
val (url, location) = decodeMagicRequestOrUrl(magicOrUrl)
|
||||
if (location != null) {
|
||||
val thumb = withIOContext {
|
||||
val legacyThumb = downloadLocation / location / LEGACY_THUMB_FILE
|
||||
if (legacyThumb.isFile) {
|
||||
legacyThumb.apply { renameTo(THUMB_FILE) }
|
||||
} else {
|
||||
downloadLocation / location / THUMB_FILE
|
||||
}
|
||||
}
|
||||
if (withIOContext { thumb.isFile }) {
|
||||
val new = chain.request.newBuilder().data(thumb.uri.toString()).build()
|
||||
val thumb = downloadLocation / location / THUMB_FILE
|
||||
if (thumb.isFile) {
|
||||
val new = chain.request.newBuilder().data(thumb.toUri()).build()
|
||||
val result = chain.withRequest(new).proceed()
|
||||
if (result is SuccessResult) return result
|
||||
}
|
||||
val new = chain.request.newBuilder().data(url).build()
|
||||
val result = chain.withRequest(new).proceed()
|
||||
if (result is SuccessResult) {
|
||||
withIOContext {
|
||||
// Accessing the recreated file immediately after deleting it throws
|
||||
// FileNotFoundException, so we just overwrite the existing file.
|
||||
if (thumb.ensureFile()) {
|
||||
val key = requireNotNull(chain.request.memoryCacheKey)
|
||||
imageCache.read(key) {
|
||||
data.asUniFile() sendTo thumb
|
||||
}
|
||||
}
|
||||
if (result is SuccessResult && thumb.parent?.isDirectory == true) {
|
||||
// Accessing the recreated file immediately after deleting it throws
|
||||
// FileNotFoundException, so we just overwrite the existing file.
|
||||
val key = requireNotNull(chain.request.memoryCacheKey)
|
||||
imageCache.read(key) {
|
||||
data sendTo thumb
|
||||
}
|
||||
}
|
||||
return result
|
||||
|
||||
@@ -2,7 +2,7 @@ package com.hippo.ehviewer.coil
|
||||
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import coil3.asCoilImage
|
||||
import coil3.asImage
|
||||
import coil3.decode.DecodeResult
|
||||
import coil3.decode.Decoder
|
||||
import coil3.network.NetworkHeaders
|
||||
@@ -16,7 +16,7 @@ import com.hippo.ehviewer.client.CHROME_ACCEPT_LANGUAGE
|
||||
import com.hippo.ehviewer.client.data.GalleryInfo
|
||||
import com.hippo.ehviewer.client.data.GalleryPreview
|
||||
import com.hippo.ehviewer.client.data.NormalGalleryPreview
|
||||
import com.hippo.ehviewer.client.url
|
||||
import com.hippo.ehviewer.client.imageKey
|
||||
import com.hippo.ehviewer.spider.DownloadInfoMagics.encodeMagicRequestOrUrl
|
||||
import io.ktor.http.HttpHeaders
|
||||
|
||||
@@ -42,7 +42,7 @@ fun ImageRequest.Builder.ehPreview(preview: GalleryPreview) = apply {
|
||||
httpHeaders(header)
|
||||
}
|
||||
|
||||
private val stubImage = ColorDrawable(Color.BLACK).asCoilImage(true)
|
||||
private val stubImage = ColorDrawable(Color.BLACK).asImage(true)
|
||||
private val stubResult = DecodeResult(stubImage, false)
|
||||
private val stubFactory = Decoder { stubResult }
|
||||
|
||||
|
||||
@@ -1,24 +1,48 @@
|
||||
package com.hippo.ehviewer.coil
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import coil3.Extras
|
||||
import coil3.asImage
|
||||
import coil3.getExtra
|
||||
import coil3.intercept.Interceptor
|
||||
import coil3.intercept.Interceptor.Chain
|
||||
import coil3.request.ErrorResult
|
||||
import coil3.request.ImageRequest
|
||||
import coil3.request.ImageResult
|
||||
import coil3.request.SuccessResult
|
||||
import coil3.request.allowHardware
|
||||
|
||||
/**
|
||||
* Allocating hardware bitmaps may fail on extremely long images.
|
||||
*
|
||||
* Retry with [allowHardware] disabled.
|
||||
*/
|
||||
private val hardwareThresholdKey = Extras.Key(default = 16384)
|
||||
|
||||
fun ImageRequest.Builder.hardwareThreshold(size: Int) = apply {
|
||||
extras[hardwareThresholdKey] = size
|
||||
}
|
||||
|
||||
val ImageRequest.hardwareThreshold: Int
|
||||
get() = getExtra(hardwareThresholdKey)
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
object HardwareBitmapInterceptor : Interceptor {
|
||||
override suspend fun intercept(chain: Chain): ImageResult {
|
||||
val result = chain.proceed()
|
||||
return if (chain.request.allowHardware && result is ErrorResult && result.throwable is OutOfMemoryError) {
|
||||
val req = chain.request.newBuilder().allowHardware(false).build()
|
||||
chain.withRequest(req).proceed()
|
||||
} else {
|
||||
result
|
||||
val request = result.request
|
||||
if (!request.allowHardware && result is SuccessResult) {
|
||||
val image = result.image
|
||||
if (image is BitmapImageWithExtraInfo) {
|
||||
val bitmap = image.image.bitmap
|
||||
val isHardware = bitmap.config == Bitmap.Config.HARDWARE
|
||||
// Large hardware bitmaps have rendering issues (e.g. crash, empty) on some devices.
|
||||
// This is not ideal but I haven't figured out how to probe the threshold.
|
||||
// All we know is that it's less than the maximum texture size.
|
||||
if (!isHardware && maxOf(bitmap.width, bitmap.height) <= request.hardwareThreshold) {
|
||||
bitmap.copy(Bitmap.Config.HARDWARE, false)?.let { hwBitmap ->
|
||||
bitmap.recycle()
|
||||
return result.copy(image = image.copy(image = hwBitmap.asImage()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.hippo.ehviewer.coil
|
||||
|
||||
import androidx.compose.ui.unit.IntRect
|
||||
import coil3.BitmapImage
|
||||
import coil3.Image
|
||||
import coil3.intercept.Interceptor
|
||||
import coil3.request.ImageResult
|
||||
import coil3.request.SuccessResult
|
||||
import coil3.request.allowHardware
|
||||
|
||||
data class BitmapImageWithExtraInfo(
|
||||
val image: BitmapImage,
|
||||
val rect: IntRect = IntRect(0, 0, image.width, image.height),
|
||||
val hasQrCode: Boolean = false,
|
||||
) : Image by image
|
||||
|
||||
object MapExtraInfoInterceptor : Interceptor {
|
||||
override suspend fun intercept(chain: Interceptor.Chain): ImageResult {
|
||||
val result = chain.proceed()
|
||||
val needMap = with(chain.request) { maybeCropBorder || detectQrCode || !allowHardware }
|
||||
if (needMap && result is SuccessResult) {
|
||||
val image = result.image
|
||||
if (image is BitmapImage) {
|
||||
return result.copy(image = BitmapImageWithExtraInfo(image))
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package com.hippo.ehviewer.coil
|
||||
import coil3.intercept.Interceptor
|
||||
import coil3.request.ImageResult
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.client.isNormalPreviewKey
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
import kotlinx.coroutines.CancellableContinuation
|
||||
@@ -12,7 +13,6 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
object MergeInterceptor : Interceptor {
|
||||
private val pendingContinuationMap: HashMap<String, MutableList<CancellableContinuation<Unit>>> = hashMapOf()
|
||||
@@ -24,7 +24,9 @@ object MergeInterceptor : Interceptor {
|
||||
notifyScope.launch {
|
||||
synchronized(pendingContinuationMapLock) {
|
||||
// Wake up a pending continuation to continue executing task
|
||||
val successor = pendingContinuationMap[key]?.removeFirstOrNull()?.apply { resume(Unit) { triggerSuccessor(key) } }
|
||||
val successor = pendingContinuationMap[key]?.removeFirstOrNull()?.apply {
|
||||
resume(Unit) { _, _, _ -> triggerSuccessor(key) }
|
||||
}
|
||||
// If no successor, delete this entry from hashmap
|
||||
successor ?: pendingContinuationMap.remove(key)
|
||||
}
|
||||
@@ -41,14 +43,14 @@ object MergeInterceptor : Interceptor {
|
||||
|
||||
override suspend fun intercept(chain: Interceptor.Chain): ImageResult {
|
||||
val req = chain.request
|
||||
val key = req.memoryCacheKey?.takeIf { it.startsWith("m/") || Settings.preloadThumbAggressively } ?: return withContext(req.interceptorDispatcher) { chain.proceed() }
|
||||
val key = req.memoryCacheKey?.takeIf { it.isNormalPreviewKey || Settings.preloadThumbAggressively } ?: return chain.proceed()
|
||||
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
synchronized(pendingContinuationMapLock) {
|
||||
val existPendingContinuations = pendingContinuationMap[key]
|
||||
if (existPendingContinuations == null) {
|
||||
pendingContinuationMap[key] = EMPTY_LIST
|
||||
continuation.resume(Unit) {
|
||||
continuation.resume(Unit) { _, _, _ ->
|
||||
triggerSuccessor(key)
|
||||
}
|
||||
} else {
|
||||
@@ -62,7 +64,7 @@ object MergeInterceptor : Interceptor {
|
||||
}
|
||||
|
||||
try {
|
||||
return withContext(req.interceptorDispatcher) { chain.proceed() }.apply {
|
||||
return chain.proceed().apply {
|
||||
// Wake all pending continuations shared with the same memory key since we have written it to memory cache
|
||||
notifyScope.launch {
|
||||
synchronized(pendingContinuationMapLock) {
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.hippo.ehviewer.coil
|
||||
|
||||
import coil3.Extras
|
||||
import coil3.getExtra
|
||||
import coil3.intercept.Interceptor
|
||||
import coil3.request.ImageRequest
|
||||
import coil3.request.ImageResult
|
||||
import coil3.request.SuccessResult
|
||||
import com.hippo.ehviewer.image.hasQrCode
|
||||
|
||||
private val detectQrCodeKey = Extras.Key(default = false)
|
||||
|
||||
fun ImageRequest.Builder.detectQrCode(enable: Boolean) = apply {
|
||||
extras[detectQrCodeKey] = enable
|
||||
}
|
||||
|
||||
val ImageRequest.detectQrCode: Boolean
|
||||
get() = getExtra(detectQrCodeKey)
|
||||
|
||||
object QrCodeInterceptor : Interceptor {
|
||||
override suspend fun intercept(chain: Interceptor.Chain): ImageResult {
|
||||
val result = chain.proceed()
|
||||
if (chain.request.detectQrCode && result is SuccessResult) {
|
||||
val image = result.image
|
||||
if (image is BitmapImageWithExtraInfo) {
|
||||
val hasQrCode = hasQrCode(image.image.bitmap)
|
||||
val new = image.copy(hasQrCode = hasQrCode)
|
||||
return result.copy(image = new)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package com.hippo.ehviewer.cronet
|
||||
|
||||
import io.ktor.utils.io.pool.DirectByteBufferPool
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.asExecutor
|
||||
|
||||
val pool = DirectByteBufferPool(32)
|
||||
|
||||
// Limit thread to 1 since we are async & non-blocking
|
||||
val cronetDispatcher = Dispatchers.Default.limitedParallelism(1)
|
||||
val cronetHttpClientExecutor = cronetDispatcher.asExecutor()
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.hippo.ehviewer.cronet
|
||||
|
||||
import com.hippo.ehviewer.Settings
|
||||
import okio.Path.Companion.toOkioPath
|
||||
import java.io.File
|
||||
import org.chromium.net.CronetEngine
|
||||
import splitties.init.appCtx
|
||||
|
||||
@@ -10,8 +10,8 @@ val cronetHttpClient: CronetEngine = CronetEngine.Builder(appCtx).apply {
|
||||
setUserAgent(Settings.userAgent)
|
||||
|
||||
// Cache Quic hint only since the real cache mechanism should on Ktor layer
|
||||
val cache = (appCtx.cacheDir.toOkioPath() / "http_cache").toFile().apply { mkdirs() }
|
||||
setStoragePath(cache.absolutePath)
|
||||
val cache = File(appCtx.cacheDir, "http_cache").apply { mkdirs() }
|
||||
setStoragePath(cache.path)
|
||||
enableHttpCache(CronetEngine.Builder.HTTP_CACHE_DISK_NO_HTTP, 4096)
|
||||
|
||||
addQuicHint("e-hentai.org", 443, 443)
|
||||
|
||||
@@ -21,7 +21,6 @@ import androidx.work.WorkManager
|
||||
import androidx.work.WorkerParameters
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.client.EhCookieStore
|
||||
import com.hippo.ehviewer.client.EhEngine
|
||||
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
@@ -38,8 +37,7 @@ import splitties.init.appCtx
|
||||
|
||||
private const val CHANNEL_ID = "DailyCheckNotification"
|
||||
|
||||
class DailyCheckWork(context: Context, workerParams: WorkerParameters) :
|
||||
CoroutineWorker(context, workerParams) {
|
||||
class DailyCheckWork(context: Context, workerParams: WorkerParameters) : CoroutineWorker(context, workerParams) {
|
||||
override suspend fun doWork(): Result = withIOContext {
|
||||
checkDawn().onFailure {
|
||||
return@withIOContext Result.retry()
|
||||
@@ -83,7 +81,7 @@ val today
|
||||
get() = (Clock.System.now().epochSeconds / 86400).toInt()
|
||||
|
||||
suspend fun checkDawn() = runCatching {
|
||||
if (EhCookieStore.hasSignedIn() && Settings.lastDawnDays != today) {
|
||||
if (Settings.hasSignedIn.value && Settings.lastDawnDays != today) {
|
||||
EhEngine.getNews(true)?.let {
|
||||
Settings.lastDawnDays = today
|
||||
showEventNotification(it)
|
||||
|
||||
@@ -8,11 +8,11 @@ import com.hippo.ehviewer.client.data.BaseGalleryInfo
|
||||
|
||||
@Database(
|
||||
entities = [
|
||||
BaseGalleryInfo::class, DownloadLabel::class, DownloadEntity::class, DownloadDirname::class,
|
||||
BaseGalleryInfo::class, DownloadLabel::class, DownloadEntity::class, DownloadDirname::class, DownloadArtist::class,
|
||||
Filter::class, HistoryInfo::class, LocalFavoriteInfo::class, ProgressInfo::class, QuickSearch::class,
|
||||
GalleryEntityFts::class,
|
||||
],
|
||||
version = 22,
|
||||
version = 23,
|
||||
exportSchema = true,
|
||||
autoMigrations = [
|
||||
AutoMigration(from = 4, to = 5, spec = Schema4to5::class),
|
||||
@@ -32,11 +32,13 @@ import com.hippo.ehviewer.client.data.BaseGalleryInfo
|
||||
AutoMigration(from = 19, to = 20),
|
||||
AutoMigration(from = 20, to = 21, spec = Schema20to21::class),
|
||||
AutoMigration(from = 21, to = 22, spec = Schema21to22::class),
|
||||
AutoMigration(from = 22, to = 23),
|
||||
],
|
||||
)
|
||||
@TypeConverters(FilterModeConverter::class)
|
||||
abstract class EhDatabase : RoomDatabase() {
|
||||
abstract fun galleryDao(): GalleryDao
|
||||
abstract fun downloadArtistDao(): DownloadArtistDao
|
||||
abstract fun downloadDirnameDao(): DownloadDirnameDao
|
||||
abstract fun downloadLabelDao(): DownloadLabelDao
|
||||
abstract fun downloadsDao(): DownloadsDao
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.hippo.ehviewer.dao
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
|
||||
@Entity(
|
||||
tableName = "DOWNLOAD_ARTISTS",
|
||||
primaryKeys = ["GID", "ARTIST"],
|
||||
foreignKeys = [
|
||||
ForeignKey(DownloadEntity::class, ["GID"], ["GID"], onDelete = ForeignKey.CASCADE),
|
||||
],
|
||||
)
|
||||
data class DownloadArtist(
|
||||
@ColumnInfo(name = "GID")
|
||||
val gid: Long,
|
||||
|
||||
@ColumnInfo(name = "ARTIST")
|
||||
val artist: String,
|
||||
) {
|
||||
companion object {
|
||||
fun from(gid: Long, artists: Iterable<String>) = artists.map { DownloadArtist(gid, it) }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.hippo.ehviewer.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
|
||||
@Dao
|
||||
interface DownloadArtistDao {
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
suspend fun insertOrIgnore(downloadArtists: List<DownloadArtist>)
|
||||
|
||||
@Query("DELETE FROM DOWNLOAD_ARTISTS WHERE GID = :gid")
|
||||
suspend fun deleteByGid(gid: Long)
|
||||
}
|
||||
@@ -67,9 +67,14 @@ data class DownloadInfo(
|
||||
@ColumnInfo(name = "DIRNAME")
|
||||
val dirname: String?,
|
||||
|
||||
@Relation(parentColumn = "GID", entityColumn = "GID")
|
||||
var artistInfoList: List<DownloadArtist> = emptyList(),
|
||||
|
||||
@Embedded
|
||||
val downloadInfo: DownloadEntity = DownloadEntity(galleryInfo.gid),
|
||||
) : GalleryInfo by galleryInfo, AbstractDownloadInfo by downloadInfo {
|
||||
) : GalleryInfo by galleryInfo,
|
||||
AbstractDownloadInfo by downloadInfo {
|
||||
|
||||
companion object {
|
||||
const val STATE_INVALID = -1
|
||||
const val STATE_NONE = 0
|
||||
@@ -79,3 +84,6 @@ data class DownloadInfo(
|
||||
const val STATE_FAILED = 4
|
||||
}
|
||||
}
|
||||
|
||||
val DownloadInfo.artists: List<String>
|
||||
get() = artistInfoList.map { it.artist }
|
||||
@@ -14,7 +14,11 @@ import kotlinx.coroutines.flow.Flow
|
||||
interface DownloadsDao {
|
||||
@Suppress("ktlint:standard:annotation")
|
||||
@Query("SELECT LABEL, COUNT(*) AS COUNT FROM DOWNLOADS LEFT JOIN DOWNLOAD_LABELS USING(LABEL) GROUP BY LABEL")
|
||||
fun count(): Flow<Map<@MapColumn("LABEL") String?, @MapColumn("COUNT") Int>>
|
||||
fun countByLabel(): Flow<Map<@MapColumn("LABEL") String?, @MapColumn("COUNT") Int>>
|
||||
|
||||
@Suppress("ktlint:standard:annotation")
|
||||
@Query("SELECT ARTIST, COUNT(*) AS COUNT FROM DOWNLOADS LEFT JOIN DOWNLOAD_ARTISTS USING(GID) GROUP BY ARTIST ORDER BY COUNT DESC, ARTIST")
|
||||
fun countByArtist(): Flow<Map<@MapColumn("ARTIST") String?, @MapColumn("COUNT") Int>>
|
||||
|
||||
@Query("SELECT * FROM DOWNLOADS ORDER BY TIME")
|
||||
suspend fun list(): List<DownloadEntity>
|
||||
|
||||
@@ -10,7 +10,7 @@ import com.hippo.ehviewer.client.data.BaseGalleryInfo
|
||||
|
||||
@Dao
|
||||
interface HistoryDao {
|
||||
@Query("SELECT * FROM HISTORY ORDER BY TIME")
|
||||
@Query("SELECT HISTORY.* FROM HISTORY JOIN GALLERIES USING(GID) ORDER BY TIME")
|
||||
suspend fun list(): List<HistoryInfo>
|
||||
|
||||
@Query("SELECT GALLERIES.* FROM HISTORY JOIN GALLERIES USING(GID) ORDER BY TIME DESC")
|
||||
|
||||
@@ -14,7 +14,7 @@ interface LocalFavoritesDao {
|
||||
@Query("SELECT COUNT(*) FROM LOCAL_FAVORITES")
|
||||
fun count(): Flow<Int>
|
||||
|
||||
@Query("SELECT * FROM LOCAL_FAVORITES ORDER BY TIME")
|
||||
@Query("SELECT LOCAL_FAVORITES.* FROM LOCAL_FAVORITES JOIN GALLERIES USING(GID) ORDER BY TIME")
|
||||
suspend fun list(): List<LocalFavoriteInfo>
|
||||
|
||||
@Query("SELECT GALLERIES.* FROM LOCAL_FAVORITES JOIN GALLERIES USING(GID) ORDER BY TIME DESC")
|
||||
@@ -27,7 +27,7 @@ interface LocalFavoritesDao {
|
||||
fun joinListLazy(title: String): PagingSource<Int, BaseGalleryInfo>
|
||||
|
||||
@Query("SELECT GALLERIES.* FROM LOCAL_FAVORITES JOIN GALLERIES USING(GID) ORDER BY RANDOM() LIMIT 1")
|
||||
suspend fun random(): BaseGalleryInfo
|
||||
suspend fun random(): BaseGalleryInfo?
|
||||
|
||||
@Query("SELECT EXISTS(SELECT * FROM LOCAL_FAVORITES WHERE GID = :gid)")
|
||||
suspend fun contains(gid: Long): Boolean
|
||||
|
||||
@@ -12,11 +12,11 @@ interface SearchDao {
|
||||
@Query("DELETE FROM suggestions WHERE `query` = :query")
|
||||
suspend fun deleteQuery(query: String)
|
||||
|
||||
@Query("SELECT * FROM suggestions WHERE `query` LIKE :prefix || '%' ORDER BY date DESC LIMIT :limit")
|
||||
suspend fun rawSuggestions(prefix: String, limit: Int): Array<Search>
|
||||
@Query("SELECT DISTINCT `query` FROM suggestions WHERE `query` LIKE :prefix || '%' ORDER BY date DESC LIMIT :limit")
|
||||
suspend fun rawSuggestions(prefix: String, limit: Int): List<String>
|
||||
|
||||
@Query("SELECT * FROM suggestions ORDER BY date DESC LIMIT :limit")
|
||||
suspend fun list(limit: Int): Array<Search>
|
||||
@Query("SELECT DISTINCT `query` FROM suggestions ORDER BY date DESC LIMIT :limit")
|
||||
suspend fun list(limit: Int): List<String>
|
||||
|
||||
@Insert
|
||||
suspend fun insert(search: Search)
|
||||
|
||||
@@ -30,6 +30,7 @@ import com.hippo.ehviewer.EhDB
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.client.data.BaseGalleryInfo
|
||||
import com.hippo.ehviewer.client.data.GalleryInfo
|
||||
import com.hippo.ehviewer.dao.DownloadArtist
|
||||
import com.hippo.ehviewer.dao.DownloadInfo
|
||||
import com.hippo.ehviewer.dao.DownloadLabel
|
||||
import com.hippo.ehviewer.image.Image
|
||||
@@ -37,17 +38,18 @@ import com.hippo.ehviewer.spider.COMIC_INFO_FILE
|
||||
import com.hippo.ehviewer.spider.SpiderQueen
|
||||
import com.hippo.ehviewer.spider.SpiderQueen.Companion.SPIDER_INFO_FILENAME
|
||||
import com.hippo.ehviewer.spider.SpiderQueen.OnSpiderListener
|
||||
import com.hippo.ehviewer.spider.putToDownloadDir
|
||||
import com.hippo.ehviewer.spider.downloadDirname
|
||||
import com.hippo.ehviewer.spider.readComicInfo
|
||||
import com.hippo.ehviewer.spider.readCompatFromUniFile
|
||||
import com.hippo.ehviewer.spider.readCompatFromPath
|
||||
import com.hippo.ehviewer.spider.toSimpleTags
|
||||
import com.hippo.ehviewer.util.AppConfig
|
||||
import com.hippo.ehviewer.util.insertWith
|
||||
import com.hippo.ehviewer.util.mapNotNull
|
||||
import com.hippo.ehviewer.util.runAssertingNotMainThread
|
||||
import com.hippo.unifile.UniFile
|
||||
import com.hippo.unifile.asUniFile
|
||||
import com.hippo.unifile.asUniFileOrNull
|
||||
import com.hippo.files.delete
|
||||
import com.hippo.files.find
|
||||
import com.hippo.files.toOkioPath
|
||||
import com.hippo.files.toUri
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -60,6 +62,9 @@ import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import logcat.LogPriority
|
||||
import okio.Path
|
||||
import okio.Path.Companion.toOkioPath
|
||||
import okio.Path.Companion.toPath
|
||||
import splitties.preferences.edit
|
||||
|
||||
object DownloadManager : OnSpiderListener, CoroutineScope {
|
||||
@@ -171,10 +176,9 @@ object DownloadManager : OnSpiderListener, CoroutineScope {
|
||||
}
|
||||
} else {
|
||||
// It is new download info
|
||||
info = DownloadInfo(galleryInfo, galleryInfo.putToDownloadDir())
|
||||
info = DownloadInfo(galleryInfo, galleryInfo.downloadDirname())
|
||||
info.label = label
|
||||
info.state = DownloadInfo.STATE_WAIT
|
||||
|
||||
// Add to all download list and map
|
||||
allInfoList.insertWith(info, comparator())
|
||||
mAllInfoMap[galleryInfo.gid] = info
|
||||
@@ -266,6 +270,7 @@ object DownloadManager : OnSpiderListener, CoroutineScope {
|
||||
|
||||
// Save to
|
||||
EhDB.putDownloadInfo(info)
|
||||
EhDB.putDownloadArtist(info.gid, info.artistInfoList)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -530,26 +535,63 @@ object DownloadManager : OnSpiderListener, CoroutineScope {
|
||||
}
|
||||
|
||||
suspend fun readMetadataFromLocal() {
|
||||
val list = allInfoList.filter { it.pages == 0 || it.simpleTags == null }.parMapNotNull(concurrency = 5) { info ->
|
||||
val list = allInfoList.mapNotNull {
|
||||
val updateGallery = it.pages == 0 || it.simpleTags == null
|
||||
val updateArtist = it.artistInfoList.isEmpty()
|
||||
if (updateGallery || updateArtist) {
|
||||
Triple(it, updateGallery, updateArtist)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}.parMapNotNull(concurrency = 5) { (info, updateGallery, updateArtist) ->
|
||||
info.downloadDir?.run {
|
||||
val comicInfo = findFile(COMIC_INFO_FILE)?.let { readComicInfo(it) }
|
||||
val comicInfo = find(COMIC_INFO_FILE)?.let { readComicInfo(it) }
|
||||
if (comicInfo != null) {
|
||||
info.pages = comicInfo.pageCount
|
||||
info.simpleTags = comicInfo.toSimpleTags()
|
||||
info.galleryInfo
|
||||
val galleryInfo = if (updateGallery) {
|
||||
info.pages = comicInfo.pageCount
|
||||
info.simpleTags = comicInfo.toSimpleTags()
|
||||
info.galleryInfo
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
val artistList = if (updateArtist && comicInfo.penciller != null) {
|
||||
info.artistInfoList = DownloadArtist.from(info.gid, comicInfo.penciller)
|
||||
info.gid to info.artistInfoList
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
if (galleryInfo != null || artistList != null) {
|
||||
galleryInfo to artistList
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} else if (info.pages == 0) {
|
||||
findFile(SPIDER_INFO_FILENAME)?.let {
|
||||
readCompatFromUniFile(it)?.run {
|
||||
val galleryInfo = find(SPIDER_INFO_FILENAME)?.let {
|
||||
readCompatFromPath(it)?.run {
|
||||
info.pages = pages
|
||||
info.galleryInfo
|
||||
}
|
||||
}
|
||||
|
||||
galleryInfo?.let { it to null }
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
EhDB.updateGalleryInfo(list)
|
||||
|
||||
val galleryInfoList = mutableListOf<BaseGalleryInfo>()
|
||||
list.forEach { (info, artists) ->
|
||||
info?.let {
|
||||
galleryInfoList.add(it)
|
||||
}
|
||||
artists?.let { (gid, updateList) ->
|
||||
EhDB.putDownloadArtist(gid, updateList)
|
||||
}
|
||||
}
|
||||
if (galleryInfoList.isNotEmpty()) EhDB.updateGalleryInfo(galleryInfoList)
|
||||
}
|
||||
|
||||
val isIdle: Boolean
|
||||
@@ -567,6 +609,7 @@ object DownloadManager : OnSpiderListener, CoroutineScope {
|
||||
override fun onGet509(index: Int) {
|
||||
launch {
|
||||
mDownloadListener?.onGet509()
|
||||
stopAllDownload()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -588,7 +631,7 @@ object DownloadManager : OnSpiderListener, CoroutineScope {
|
||||
info.finished = finished
|
||||
info.downloaded = downloaded
|
||||
info.total = total
|
||||
mDownloadListener?.onGetPage(info)
|
||||
mDownloadListener?.onDownload(info)
|
||||
mutableNotifyFlow.emit(info)
|
||||
} ?: logcat(TAG, LogPriority.ERROR) { "Current task is null, but it should not be" }
|
||||
}
|
||||
@@ -664,11 +707,6 @@ object DownloadManager : OnSpiderListener, CoroutineScope {
|
||||
*/
|
||||
fun onDownload(info: DownloadInfo)
|
||||
|
||||
/**
|
||||
* Update page downloaded
|
||||
*/
|
||||
fun onGetPage(info: DownloadInfo)
|
||||
|
||||
/**
|
||||
* Download done
|
||||
*/
|
||||
@@ -775,18 +813,21 @@ object DownloadManager : OnSpiderListener, CoroutineScope {
|
||||
private fun comparator() = SortMode.from(Settings.downloadSortMode.value).comparator()
|
||||
}
|
||||
|
||||
var downloadLocation: UniFile
|
||||
var downloadLocation: Path
|
||||
get() = with(Settings) {
|
||||
val uri = Uri.Builder().apply {
|
||||
scheme(downloadScheme)
|
||||
encodedAuthority(downloadAuthority)
|
||||
encodedPath(downloadPath)
|
||||
encodedQuery(downloadQuery)
|
||||
encodedFragment(downloadFragment)
|
||||
}.build()
|
||||
uri.asUniFileOrNull() ?: AppConfig.defaultDownloadDir?.asUniFile() ?: UniFile.Stub
|
||||
if (downloadScheme != null) {
|
||||
Uri.Builder().apply {
|
||||
scheme(downloadScheme)
|
||||
encodedAuthority(downloadAuthority)
|
||||
encodedPath(downloadPath)
|
||||
encodedQuery(downloadQuery)
|
||||
encodedFragment(downloadFragment)
|
||||
}.build().toOkioPath()
|
||||
} else {
|
||||
AppConfig.defaultDownloadDir?.toOkioPath() ?: "".toPath()
|
||||
}
|
||||
}
|
||||
set(value) = with(value.uri) {
|
||||
set(value) = with(value.toUri()) {
|
||||
Settings.edit {
|
||||
downloadScheme = scheme
|
||||
downloadAuthority = encodedAuthority
|
||||
@@ -797,5 +838,5 @@ var downloadLocation: UniFile
|
||||
}
|
||||
|
||||
val DownloadInfo.downloadDir get() = dirname?.let { downloadLocation / it }
|
||||
val DownloadInfo.archiveFile get() = downloadDir?.findFile("$gid.cbz")
|
||||
val GalleryInfo.tempDownloadDir get() = AppConfig.getTempDir("$gid")
|
||||
val DownloadInfo.archiveFile get() = downloadDir?.run { find("$gid.cbz") ?: find("$gid.zip") }
|
||||
val GalleryInfo.tempDownloadDir get() = AppConfig.getTempDir("$gid")?.toOkioPath()
|
||||
@@ -21,8 +21,6 @@ import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Bundle
|
||||
import android.os.SystemClock
|
||||
import androidx.annotation.IntDef
|
||||
import androidx.collection.LongSparseArray
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.app.NotificationChannelCompat
|
||||
@@ -34,58 +32,59 @@ import com.hippo.ehviewer.client.EhUtils
|
||||
import com.hippo.ehviewer.client.data.BaseGalleryInfo
|
||||
import com.hippo.ehviewer.dao.DownloadInfo
|
||||
import com.hippo.ehviewer.ui.MainActivity
|
||||
import com.hippo.ehviewer.ui.screen.DownloadsFragment
|
||||
import com.hippo.ehviewer.util.FileUtils
|
||||
import com.hippo.ehviewer.util.ReadableTime
|
||||
import com.hippo.ehviewer.util.SimpleHandler
|
||||
import com.hippo.ehviewer.util.getParcelableExtraCompat
|
||||
import com.hippo.ehviewer.util.unsafeLazy
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.flow.sample
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class DownloadService : Service(), DownloadManager.DownloadListener, CoroutineScope {
|
||||
class DownloadService :
|
||||
Service(),
|
||||
DownloadManager.DownloadListener,
|
||||
CoroutineScope {
|
||||
override val coroutineContext = Dispatchers.IO + SupervisorJob()
|
||||
private val deferredMgr = async { DownloadManager.apply { setDownloadListener(this@DownloadService) } }
|
||||
private var mNotifyManager: NotificationManagerCompat? = null
|
||||
private var mDownloadingBuilder: NotificationCompat.Builder? = null
|
||||
private var mDownloadedBuilder: NotificationCompat.Builder? = null
|
||||
private var m509dBuilder: NotificationCompat.Builder? = null
|
||||
private var mDownloadingDelay: NotificationDelay? = null
|
||||
private var mDownloadedDelay: NotificationDelay? = null
|
||||
private var m509Delay: NotificationDelay? = null
|
||||
private var mChannelID: String? = null
|
||||
private val deferredMgr = async { DownloadManager }
|
||||
private val notifyManager by unsafeLazy { NotificationManagerCompat.from(this) }
|
||||
private val downloadingNotification by unsafeLazy { initDownloadingNotification() }
|
||||
private val downloadedNotification by lazy { initDownloadedNotification() }
|
||||
private val error509Notification by lazy { init509Notification() }
|
||||
private val channelId by unsafeLazy { "$packageName.download" }
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
mChannelID = "$packageName.download"
|
||||
mNotifyManager = NotificationManagerCompat.from(this)
|
||||
mNotifyManager!!.createNotificationChannel(
|
||||
NotificationChannelCompat.Builder(mChannelID!!, NotificationManagerCompat.IMPORTANCE_LOW)
|
||||
notifyManager.createNotificationChannel(
|
||||
NotificationChannelCompat.Builder(channelId, NotificationManagerCompat.IMPORTANCE_LOW)
|
||||
.setName(getString(R.string.download_service)).build(),
|
||||
)
|
||||
ensureDownloadingBuilder()
|
||||
mDownloadingBuilder!!.setContentTitle(getString(R.string.download_service))
|
||||
.setContentText(null)
|
||||
.setSubText(null)
|
||||
.setProgress(0, 0, true)
|
||||
startForeground(ID_DOWNLOADING, mDownloadingBuilder!!.build())
|
||||
downloadingNotification.builder.run {
|
||||
setContentTitle(getString(R.string.download_service))
|
||||
.setContentText(null)
|
||||
.setSubText(null)
|
||||
.setProgress(0, 0, true)
|
||||
startForeground(ID_DOWNLOADING, build())
|
||||
}
|
||||
launch {
|
||||
deferredMgr.await().setDownloadListener(this@DownloadService)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
val scope = this
|
||||
launch {
|
||||
deferredMgr.await().setDownloadListener(null)
|
||||
// Wait for the last notification to be posted
|
||||
delay(DELAY)
|
||||
scope.cancel()
|
||||
}
|
||||
mNotifyManager = null
|
||||
mDownloadingBuilder = null
|
||||
mDownloadedBuilder = null
|
||||
m509dBuilder = null
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
@@ -153,17 +152,14 @@ class DownloadService : Service(), DownloadManager.DownloadListener, CoroutineSc
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent) = error("No bindService")
|
||||
override fun onBind(intent: Intent) = null
|
||||
|
||||
private fun ensureDownloadingBuilder() {
|
||||
if (mDownloadingBuilder != null) {
|
||||
return
|
||||
}
|
||||
private fun initDownloadingNotification(): NotificationHandler {
|
||||
val stopAllIntent = Intent(this, DownloadService::class.java)
|
||||
stopAllIntent.action = ACTION_STOP_ALL
|
||||
val piStopAll =
|
||||
PendingIntent.getService(this, 0, stopAllIntent, PendingIntent.FLAG_IMMUTABLE)
|
||||
mDownloadingBuilder = NotificationCompat.Builder(applicationContext, mChannelID!!)
|
||||
val builder = NotificationCompat.Builder(applicationContext, channelId)
|
||||
.setSmallIcon(android.R.drawable.stat_sys_download)
|
||||
.setOngoing(true)
|
||||
.setAutoCancel(false)
|
||||
@@ -174,47 +170,40 @@ class DownloadService : Service(), DownloadManager.DownloadListener, CoroutineSc
|
||||
piStopAll,
|
||||
)
|
||||
.setShowWhen(false)
|
||||
.setChannelId(mChannelID!!)
|
||||
mDownloadingDelay =
|
||||
NotificationDelay(this, mNotifyManager!!, mDownloadingBuilder!!, ID_DOWNLOADING)
|
||||
return NotificationHandler(this, notifyManager, builder, ID_DOWNLOADING)
|
||||
.apply { launch { run() } }
|
||||
}
|
||||
|
||||
private fun ensureDownloadedBuilder() {
|
||||
if (mDownloadedBuilder != null) {
|
||||
return
|
||||
}
|
||||
private fun initDownloadedNotification(): NotificationHandler {
|
||||
val clearIntent = Intent(this, DownloadService::class.java)
|
||||
clearIntent.action = ACTION_CLEAR
|
||||
val piClear = PendingIntent.getService(this, 0, clearIntent, PendingIntent.FLAG_IMMUTABLE)
|
||||
val bundle = Bundle()
|
||||
bundle.putString(DownloadsFragment.KEY_ACTION, DownloadsFragment.ACTION_CLEAR_DOWNLOAD_SERVICE)
|
||||
bundle.putString(KEY_ACTION, ACTION_CLEAR_DOWNLOAD_SERVICE)
|
||||
val activityIntent = Intent(this, MainActivity::class.java)
|
||||
activityIntent.action = ACTION_START_DOWNLOADSCENE
|
||||
activityIntent.putExtra(ACTION_START_DOWNLOADSCENE_ARGS, bundle)
|
||||
val piActivity = PendingIntent.getActivity(
|
||||
this@DownloadService,
|
||||
this,
|
||||
0,
|
||||
activityIntent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
|
||||
)
|
||||
mDownloadedBuilder = NotificationCompat.Builder(applicationContext, mChannelID!!)
|
||||
val builder = NotificationCompat.Builder(applicationContext, channelId)
|
||||
.setSmallIcon(android.R.drawable.stat_sys_download_done)
|
||||
.setContentTitle(getString(R.string.stat_download_done_title))
|
||||
.setDeleteIntent(piClear)
|
||||
.setOngoing(false)
|
||||
.setAutoCancel(true)
|
||||
.setContentIntent(piActivity)
|
||||
mDownloadedDelay =
|
||||
NotificationDelay(this, mNotifyManager!!, mDownloadedBuilder!!, ID_DOWNLOADED)
|
||||
return NotificationHandler(this, notifyManager, builder, ID_DOWNLOADED)
|
||||
.apply { launch { run() } }
|
||||
}
|
||||
|
||||
private fun ensure509Builder() {
|
||||
if (m509dBuilder != null) {
|
||||
return
|
||||
}
|
||||
m509dBuilder = NotificationCompat.Builder(applicationContext, mChannelID!!)
|
||||
private fun init509Notification(): NotificationHandler {
|
||||
val builder = NotificationCompat.Builder(applicationContext, channelId)
|
||||
.setSmallIcon(R.drawable.ic_baseline_warning_24)
|
||||
.setContentText(getString(R.string.stat_509_alert_title))
|
||||
.setContentTitle(getString(R.string.stat_509_alert_title))
|
||||
.setContentText(getString(R.string.stat_509_alert_text))
|
||||
.setStyle(
|
||||
NotificationCompat.BigTextStyle().bigText(getString(R.string.stat_509_alert_text)),
|
||||
@@ -222,89 +211,61 @@ class DownloadService : Service(), DownloadManager.DownloadListener, CoroutineSc
|
||||
.setAutoCancel(true)
|
||||
.setOngoing(false)
|
||||
.setCategory(NotificationCompat.CATEGORY_ERROR)
|
||||
m509Delay = NotificationDelay(this, mNotifyManager!!, m509dBuilder!!, ID_509)
|
||||
return NotificationHandler(this, notifyManager, builder, ID_509)
|
||||
.apply { launch { run() } }
|
||||
}
|
||||
|
||||
override fun onGet509() {
|
||||
launch {
|
||||
deferredMgr.await().stopAllDownload()
|
||||
if (mNotifyManager == null) {
|
||||
return@launch
|
||||
}
|
||||
ensure509Builder()
|
||||
m509dBuilder!!.setWhen(System.currentTimeMillis())
|
||||
m509Delay!!.show()
|
||||
error509Notification.run {
|
||||
builder.setWhen(System.currentTimeMillis())
|
||||
show()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStart(info: DownloadInfo) {
|
||||
if (mNotifyManager == null) {
|
||||
return
|
||||
}
|
||||
ensureDownloadingBuilder()
|
||||
val bundle = Bundle()
|
||||
bundle.putLong(DownloadsFragment.KEY_GID, info.gid)
|
||||
bundle.putLong(KEY_GID, info.gid)
|
||||
val activityIntent = Intent(this, MainActivity::class.java)
|
||||
activityIntent.action = ACTION_START_DOWNLOADSCENE
|
||||
activityIntent.putExtra(ACTION_START_DOWNLOADSCENE_ARGS, bundle)
|
||||
val piActivity = PendingIntent.getActivity(
|
||||
this@DownloadService,
|
||||
this,
|
||||
0,
|
||||
activityIntent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
|
||||
)
|
||||
mDownloadingBuilder!!.setContentTitle(EhUtils.getSuitableTitle(info))
|
||||
.setContentText(null)
|
||||
.setSubText(null)
|
||||
.setProgress(0, 0, true)
|
||||
.setContentIntent(piActivity)
|
||||
mDownloadingDelay!!.startForeground()
|
||||
}
|
||||
|
||||
private fun onUpdate(info: DownloadInfo) {
|
||||
if (mNotifyManager == null) {
|
||||
return
|
||||
downloadingNotification.run {
|
||||
builder.setContentTitle(EhUtils.getSuitableTitle(info))
|
||||
.setContentText(null)
|
||||
.setSubText(null)
|
||||
.setProgress(0, 0, true)
|
||||
.setContentIntent(piActivity)
|
||||
startForeground()
|
||||
}
|
||||
ensureDownloadingBuilder()
|
||||
var speed = info.speed
|
||||
if (speed < 0) {
|
||||
speed = 0
|
||||
}
|
||||
var text = FileUtils.humanReadableByteCount(speed, false) + "/s"
|
||||
val remaining = info.remaining
|
||||
text = if (remaining >= 0) {
|
||||
getString(
|
||||
R.string.download_speed_text_2,
|
||||
text,
|
||||
ReadableTime.getShortTimeInterval(remaining),
|
||||
)
|
||||
} else {
|
||||
getString(R.string.download_speed_text, text)
|
||||
}
|
||||
mDownloadingBuilder!!.setContentTitle(EhUtils.getSuitableTitle(info))
|
||||
.setContentText(text)
|
||||
.setStyle(NotificationCompat.BigTextStyle().bigText(text))
|
||||
.setSubText(if (info.total == -1 || info.finished == -1) null else info.finished.toString() + "/" + info.total)
|
||||
.setProgress(info.total, info.finished, false)
|
||||
mDownloadingDelay!!.startForeground()
|
||||
}
|
||||
|
||||
override fun onDownload(info: DownloadInfo) {
|
||||
onUpdate(info)
|
||||
}
|
||||
|
||||
override fun onGetPage(info: DownloadInfo) {
|
||||
onUpdate(info)
|
||||
val speed = info.speed.coerceAtLeast(0)
|
||||
val speedText = FileUtils.humanReadableByteCount(speed, false) + "/s"
|
||||
val remaining = info.remaining
|
||||
val text = if (remaining >= 0) {
|
||||
val interval = ReadableTime.getShortTimeInterval(remaining)
|
||||
getString(R.string.download_speed_text_2, speedText, interval)
|
||||
} else {
|
||||
getString(R.string.download_speed_text, speedText)
|
||||
}
|
||||
downloadingNotification.run {
|
||||
builder.setContentTitle(EhUtils.getSuitableTitle(info))
|
||||
.setContentText(text)
|
||||
.setStyle(NotificationCompat.BigTextStyle().bigText(text))
|
||||
.setSubText(if (info.total == -1 || info.finished == -1) null else info.finished.toString() + "/" + info.total)
|
||||
.setProgress(info.total, info.finished, false)
|
||||
startForeground()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFinish(info: DownloadInfo) {
|
||||
if (mNotifyManager == null) {
|
||||
return
|
||||
}
|
||||
if (null != mDownloadingDelay) {
|
||||
mDownloadingDelay!!.cancel()
|
||||
}
|
||||
ensureDownloadedBuilder()
|
||||
downloadingNotification.cancel()
|
||||
val finish = info.state == DownloadInfo.STATE_FINISH
|
||||
val gid = info.gid
|
||||
val index = sItemStateArray.indexOfKey(gid)
|
||||
@@ -393,21 +354,18 @@ class DownloadService : Service(), DownloadManager.DownloadListener, CoroutineSc
|
||||
} else {
|
||||
style = null
|
||||
}
|
||||
mDownloadedBuilder!!.setContentText(text)
|
||||
.setStyle(style)
|
||||
.setWhen(System.currentTimeMillis())
|
||||
.setNumber(sDownloadedCount)
|
||||
mDownloadedDelay!!.show()
|
||||
downloadedNotification.run {
|
||||
builder.setContentText(text)
|
||||
.setStyle(style)
|
||||
.setWhen(System.currentTimeMillis())
|
||||
.setNumber(sDownloadedCount)
|
||||
show()
|
||||
}
|
||||
checkStopSelf()
|
||||
}
|
||||
|
||||
override fun onCancel(info: DownloadInfo) {
|
||||
if (mNotifyManager == null) {
|
||||
return
|
||||
}
|
||||
if (null != mDownloadingDelay) {
|
||||
mDownloadingDelay!!.cancel()
|
||||
}
|
||||
downloadingNotification.cancel()
|
||||
checkStopSelf()
|
||||
}
|
||||
|
||||
@@ -420,105 +378,49 @@ class DownloadService : Service(), DownloadManager.DownloadListener, CoroutineSc
|
||||
}
|
||||
}
|
||||
|
||||
private class NotificationDelay(
|
||||
private var mService: Service,
|
||||
private val mNotifyManager: NotificationManagerCompat,
|
||||
private val mBuilder: NotificationCompat.Builder,
|
||||
private val mId: Int,
|
||||
) : Runnable {
|
||||
private var mLastTime: Long = 0
|
||||
private var mPosted = false
|
||||
|
||||
// false for show, true for cancel
|
||||
@Ops
|
||||
private var mOps = 0
|
||||
private class NotificationHandler(
|
||||
private val service: Service,
|
||||
private val notifyManager: NotificationManagerCompat,
|
||||
val builder: NotificationCompat.Builder,
|
||||
private val id: Int,
|
||||
) {
|
||||
private val channel = Channel<Ops>(Channel.CONFLATED)
|
||||
|
||||
fun show() {
|
||||
if (mPosted) {
|
||||
mOps = OPS_NOTIFY
|
||||
} else {
|
||||
val now = SystemClock.currentThreadTimeMillis()
|
||||
if (now - mLastTime > DELAY) {
|
||||
// Wait long enough, do it now
|
||||
if (ActivityCompat.checkSelfPermission(
|
||||
mService,
|
||||
Manifest.permission.POST_NOTIFICATIONS,
|
||||
) != PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
return
|
||||
}
|
||||
mNotifyManager.notify(mId, mBuilder.build())
|
||||
} else {
|
||||
// Too quick, post delay
|
||||
mOps = OPS_NOTIFY
|
||||
mPosted = true
|
||||
SimpleHandler.postDelayed(this, DELAY)
|
||||
}
|
||||
mLastTime = now
|
||||
}
|
||||
channel.trySend(Ops.Notify)
|
||||
}
|
||||
|
||||
fun cancel() {
|
||||
if (mPosted) {
|
||||
mOps = OPS_CANCEL
|
||||
} else {
|
||||
val now = SystemClock.currentThreadTimeMillis()
|
||||
if (now - mLastTime > DELAY) {
|
||||
// Wait long enough, do it now
|
||||
mNotifyManager.cancel(mId)
|
||||
} else {
|
||||
// Too quick, post delay
|
||||
mOps = OPS_CANCEL
|
||||
mPosted = true
|
||||
SimpleHandler.postDelayed(this, DELAY)
|
||||
}
|
||||
}
|
||||
channel.trySend(Ops.Cancel)
|
||||
}
|
||||
|
||||
fun startForeground() {
|
||||
if (mPosted) {
|
||||
mOps = OPS_START_FOREGROUND
|
||||
} else {
|
||||
val now = SystemClock.currentThreadTimeMillis()
|
||||
if (now - mLastTime > DELAY) {
|
||||
// Wait long enough, do it now
|
||||
mService.startForeground(mId, mBuilder.build())
|
||||
} else {
|
||||
// Too quick, post delay
|
||||
mOps = OPS_START_FOREGROUND
|
||||
mPosted = true
|
||||
SimpleHandler.postDelayed(this, DELAY)
|
||||
}
|
||||
}
|
||||
channel.trySend(Ops.StartForeground)
|
||||
}
|
||||
|
||||
override fun run() {
|
||||
mPosted = false
|
||||
when (mOps) {
|
||||
OPS_NOTIFY -> {
|
||||
if (ActivityCompat.checkSelfPermission(
|
||||
mService,
|
||||
Manifest.permission.POST_NOTIFICATIONS,
|
||||
) != PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
return
|
||||
suspend fun run() {
|
||||
channel.receiveAsFlow().sample(DELAY).collect {
|
||||
when (it) {
|
||||
Ops.Notify -> {
|
||||
if (ActivityCompat.checkSelfPermission(
|
||||
service,
|
||||
Manifest.permission.POST_NOTIFICATIONS,
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
notifyManager.notify(id, builder.build())
|
||||
}
|
||||
}
|
||||
mNotifyManager.notify(mId, mBuilder.build())
|
||||
}
|
||||
|
||||
OPS_CANCEL -> mNotifyManager.cancel(mId)
|
||||
OPS_START_FOREGROUND -> mService.startForeground(mId, mBuilder.build())
|
||||
Ops.Cancel -> notifyManager.cancel(id)
|
||||
Ops.StartForeground -> service.startForeground(id, builder.build())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@IntDef(OPS_NOTIFY, OPS_CANCEL, OPS_START_FOREGROUND)
|
||||
@Retention(AnnotationRetention.SOURCE)
|
||||
private annotation class Ops
|
||||
companion object {
|
||||
private const val OPS_NOTIFY = 0
|
||||
private const val OPS_CANCEL = 1
|
||||
private const val OPS_START_FOREGROUND = 2
|
||||
private const val DELAY: Long = 1000 // 1s
|
||||
private enum class Ops {
|
||||
Notify,
|
||||
Cancel,
|
||||
StartForeground,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -540,9 +442,12 @@ class DownloadService : Service(), DownloadManager.DownloadListener, CoroutineSc
|
||||
const val KEY_LABEL = "label"
|
||||
const val KEY_GID = "gid"
|
||||
const val KEY_GID_LIST = "gid_list"
|
||||
const val KEY_ACTION = "action"
|
||||
const val ACTION_CLEAR_DOWNLOAD_SERVICE = "clear_download_service"
|
||||
private const val ID_DOWNLOADING = 1
|
||||
private const val ID_DOWNLOADED = 2
|
||||
private const val ID_509 = 3
|
||||
private const val DELAY = 1000L // 1s
|
||||
private val sItemStateArray = LongSparseArray<Boolean>()
|
||||
private val sItemTitleArray = LongSparseArray<String>()
|
||||
private var sFailedCount = 0
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.hippo.ehviewer.download
|
||||
|
||||
import com.hippo.ehviewer.dao.DownloadInfo
|
||||
import com.hippo.ehviewer.dao.artists
|
||||
|
||||
enum class DownloadsFilterMode(
|
||||
val flag: Int,
|
||||
val take: (info: DownloadInfo, label: String?) -> Boolean,
|
||||
) {
|
||||
CUSTOM(flag = 0, take = { info, label ->
|
||||
label == "" || info.label == label
|
||||
}),
|
||||
ARTIST(flag = 1, take = { info, label ->
|
||||
if (null == label) {
|
||||
info.artistInfoList.isEmpty()
|
||||
} else {
|
||||
label == "" || info.artists.contains(label)
|
||||
}
|
||||
}),
|
||||
;
|
||||
|
||||
companion object {
|
||||
val Default = ARTIST
|
||||
fun from(flag: Int) = DownloadsFilterMode.entries[flag]
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@
|
||||
package com.hippo.ehviewer.gallery
|
||||
|
||||
import android.os.ParcelFileDescriptor
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.Settings.archivePasswds
|
||||
import com.hippo.ehviewer.image.ByteBufferSource
|
||||
import com.hippo.ehviewer.image.Image
|
||||
@@ -28,12 +29,14 @@ import com.hippo.ehviewer.jni.openArchive
|
||||
import com.hippo.ehviewer.jni.providePassword
|
||||
import com.hippo.ehviewer.jni.releaseByteBuffer
|
||||
import com.hippo.ehviewer.util.FileUtils
|
||||
import com.hippo.unifile.UniFile
|
||||
import com.hippo.unifile.displayPath
|
||||
import com.hippo.ehviewer.util.displayPath
|
||||
import com.hippo.files.openFileDescriptor
|
||||
import com.hippo.files.toUri
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import java.nio.ByteBuffer
|
||||
import kotlinx.coroutines.CoroutineStart
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.currentCoroutineContext
|
||||
import kotlinx.coroutines.ensureActive
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -41,32 +44,32 @@ import kotlinx.coroutines.sync.Semaphore
|
||||
import kotlinx.coroutines.sync.withPermit
|
||||
import moe.tarsin.coroutines.NamedMutex
|
||||
import moe.tarsin.coroutines.withLock
|
||||
import okio.Path
|
||||
|
||||
typealias PasswdInvalidator = (String) -> Boolean
|
||||
typealias PasswdProvider = suspend (PasswdInvalidator) -> String
|
||||
|
||||
class ArchivePageLoader(
|
||||
private val file: UniFile,
|
||||
private val file: Path,
|
||||
gid: Long = 0,
|
||||
startPage: Int = 0,
|
||||
private val hasAds: Boolean = false,
|
||||
passwdProvider: PasswdProvider? = null,
|
||||
) : PageLoader2(gid, startPage) {
|
||||
private lateinit var pfd: ParcelFileDescriptor
|
||||
private val hostJob = launch(start = CoroutineStart.LAZY) {
|
||||
logcat(DEBUG_TAG) { "Open archive ${file.uri.displayPath}" }
|
||||
private val hostJob = async(start = CoroutineStart.LAZY) {
|
||||
logcat(DEBUG_TAG) { "Open archive ${file.toUri().displayPath}" }
|
||||
pfd = file.openFileDescriptor("r")
|
||||
size = openArchive(pfd.fd, pfd.statSize, gid == 0L)
|
||||
if (size == 0) {
|
||||
return@launch
|
||||
}
|
||||
if (passwdProvider != null && needPassword()) {
|
||||
val size = openArchive(pfd.fd, pfd.statSize, gid == 0L || file.name.endsWith(".zip"))
|
||||
if (size != 0 && passwdProvider != null && needPassword()) {
|
||||
archivePasswds?.forEach {
|
||||
it ?: return@forEach
|
||||
if (providePassword(it)) return@launch
|
||||
if (providePassword(it)) return@async size
|
||||
}
|
||||
val toAdd = passwdProvider { providePassword(it) }
|
||||
archivePasswds = archivePasswds?.toMutableSet()?.apply { add(toAdd) } ?: setOf(toAdd)
|
||||
}
|
||||
size
|
||||
}
|
||||
|
||||
override var size = 0
|
||||
@@ -81,7 +84,7 @@ class ArchivePageLoader(
|
||||
super.stop()
|
||||
closeArchive()
|
||||
pfd.close()
|
||||
logcat(DEBUG_TAG) { "Close archive ${file.uri.displayPath} successfully!" }
|
||||
logcat(DEBUG_TAG) { "Close archive ${file.toUri().displayPath} successfully!" }
|
||||
}
|
||||
|
||||
private val mJobMap = hashMapOf<Int, Job>()
|
||||
@@ -103,6 +106,8 @@ class ArchivePageLoader(
|
||||
}
|
||||
}
|
||||
|
||||
private fun mayBeAd(index: Int) = index > size - 10
|
||||
|
||||
private suspend fun doRealWork(index: Int) {
|
||||
val buffer = extractToByteBuffer(index) ?: return notifyPageFailed(index, null)
|
||||
check(buffer.isDirect)
|
||||
@@ -119,7 +124,7 @@ class ArchivePageLoader(
|
||||
src.close()
|
||||
throw it
|
||||
}
|
||||
val image = Image.decode(src) ?: return notifyPageFailed(index, null)
|
||||
val image = Image.decode(src, hasAds && Settings.stripExtraneousAds.value && mayBeAd(index)) ?: return notifyPageFailed(index, null)
|
||||
runCatching {
|
||||
currentCoroutineContext().ensureActive()
|
||||
}.onFailure {
|
||||
@@ -134,37 +139,29 @@ class ArchivePageLoader(
|
||||
}
|
||||
|
||||
override suspend fun awaitReady(): Boolean {
|
||||
hostJob.join()
|
||||
return super.awaitReady() && size != 0
|
||||
size = hostJob.await()
|
||||
return super.awaitReady() && isReady
|
||||
}
|
||||
|
||||
override val isReady: Boolean
|
||||
get() = size != 0
|
||||
|
||||
override fun onCancelRequest(index: Int) {
|
||||
mJobMap[index]?.cancel()
|
||||
}
|
||||
|
||||
override val title by lazy {
|
||||
FileUtils.getNameFromFilename(file.name)!!
|
||||
}
|
||||
|
||||
override fun getImageExtension(index: Int): String {
|
||||
return getExtension(index)
|
||||
}
|
||||
override fun getImageExtension(index: Int): String = getExtension(index)
|
||||
|
||||
override fun save(index: Int, file: UniFile): Boolean {
|
||||
return runCatching {
|
||||
file.openFileDescriptor("w").use {
|
||||
extractToFd(index, it.fd)
|
||||
}
|
||||
}.getOrElse {
|
||||
logcat(it)
|
||||
false
|
||||
override fun save(index: Int, file: Path): Boolean = runCatching {
|
||||
file.openFileDescriptor("w").use {
|
||||
extractToFd(index, it.fd)
|
||||
}
|
||||
}.getOrElse {
|
||||
logcat(it)
|
||||
false
|
||||
}
|
||||
|
||||
override fun preloadPages(pages: List<Int>, pair: Pair<Int, Int>) {}
|
||||
override fun prefetchPages(pages: List<Int>, bounds: Pair<Int, Int>) = Unit
|
||||
}
|
||||
|
||||
private const val DEBUG_TAG = "ArchivePageLoader"
|
||||
@@ -22,11 +22,11 @@ import com.hippo.ehviewer.spider.SpiderQueen
|
||||
import com.hippo.ehviewer.spider.SpiderQueen.Companion.obtainSpiderQueen
|
||||
import com.hippo.ehviewer.spider.SpiderQueen.Companion.releaseSpiderQueen
|
||||
import com.hippo.ehviewer.spider.SpiderQueen.OnSpiderListener
|
||||
import com.hippo.ehviewer.util.FileUtils
|
||||
import com.hippo.unifile.UniFile
|
||||
import okio.Path
|
||||
|
||||
class EhPageLoader(private val mGalleryInfo: GalleryInfo, startPage: Int) :
|
||||
PageLoader2(mGalleryInfo.gid, startPage), OnSpiderListener {
|
||||
PageLoader2(mGalleryInfo.gid, startPage),
|
||||
OnSpiderListener {
|
||||
private lateinit var mSpiderQueen: SpiderQueen
|
||||
override fun start() {
|
||||
super.start()
|
||||
@@ -43,16 +43,12 @@ class EhPageLoader(private val mGalleryInfo: GalleryInfo, startPage: Int) :
|
||||
}
|
||||
|
||||
override val title by lazy {
|
||||
FileUtils.sanitizeFilename(EhUtils.getSuitableTitle(mGalleryInfo))
|
||||
EhUtils.getSuitableTitle(mGalleryInfo)
|
||||
}
|
||||
|
||||
override fun getImageExtension(index: Int): String {
|
||||
return mSpiderQueen.getExtension(index)!!
|
||||
}
|
||||
override fun getImageExtension(index: Int): String? = mSpiderQueen.getExtension(index)
|
||||
|
||||
override fun save(index: Int, file: UniFile): Boolean {
|
||||
return mSpiderQueen.save(index, file)
|
||||
}
|
||||
override fun save(index: Int, file: Path): Boolean = mSpiderQueen.save(index, file)
|
||||
|
||||
override val size: Int
|
||||
get() = mSpiderQueen.size
|
||||
@@ -65,17 +61,11 @@ class EhPageLoader(private val mGalleryInfo: GalleryInfo, startPage: Int) :
|
||||
mSpiderQueen.forceRequest(index, orgImg)
|
||||
}
|
||||
|
||||
override suspend fun awaitReady(): Boolean {
|
||||
return super.awaitReady() && mSpiderQueen.awaitReady()
|
||||
}
|
||||
override suspend fun awaitReady(): Boolean = super.awaitReady() && mSpiderQueen.awaitReady()
|
||||
|
||||
override val isReady: Boolean
|
||||
get() = ::mSpiderQueen.isInitialized && mSpiderQueen.isReady
|
||||
|
||||
override fun onCancelRequest(index: Int) {
|
||||
mSpiderQueen.cancelRequest(index)
|
||||
}
|
||||
|
||||
override fun onGetPages(pages: Int) {}
|
||||
|
||||
override fun onGet509(index: Int) {}
|
||||
@@ -111,7 +101,7 @@ class EhPageLoader(private val mGalleryInfo: GalleryInfo, startPage: Int) :
|
||||
notifyPageFailed(index, error)
|
||||
}
|
||||
|
||||
override fun preloadPages(pages: List<Int>, pair: Pair<Int, Int>) {
|
||||
mSpiderQueen.preloadPages(pages, pair)
|
||||
override fun prefetchPages(pages: List<Int>, bounds: Pair<Int, Int>) {
|
||||
mSpiderQueen.preloadPages(pages, bounds)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.hippo.ehviewer.gallery
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import com.hippo.ehviewer.image.Image
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
||||
class Page(
|
||||
val index: Int,
|
||||
val statusFlow: MutableStateFlow<PageStatus> = MutableStateFlow(PageStatus.Queued),
|
||||
)
|
||||
|
||||
fun Page.unblock() = statusFlow.update { status ->
|
||||
when (status) {
|
||||
is PageStatus.Blocked -> PageStatus.Ready(status.ad)
|
||||
else -> error("Call unblock on page not blocked!!!")
|
||||
}
|
||||
}
|
||||
|
||||
fun Page.reset() = statusFlow.update { PageStatus.Queued }
|
||||
|
||||
val Page.status
|
||||
get() = statusFlow.value
|
||||
|
||||
val Page.statusObserved
|
||||
@Composable
|
||||
get() = statusFlow.collectAsState().value
|
||||
|
||||
val PageStatus.progressObserved
|
||||
@Composable
|
||||
get() = when (this) {
|
||||
is PageStatus.Loading -> progress.collectAsState().value
|
||||
else -> 0f
|
||||
}
|
||||
|
||||
sealed interface PageStatus {
|
||||
data object Queued : PageStatus
|
||||
data class Blocked(val ad: Image) : PageStatus
|
||||
data class Ready(val image: Image) : PageStatus
|
||||
data class Error(val message: String?) : PageStatus
|
||||
data class Loading(val progress: MutableStateFlow<Float>) : PageStatus
|
||||
}
|
||||
@@ -15,34 +15,35 @@
|
||||
*/
|
||||
package com.hippo.ehviewer.gallery
|
||||
|
||||
import androidx.annotation.CallSuper
|
||||
import com.hippo.ehviewer.EhDB
|
||||
import com.hippo.unifile.UniFile
|
||||
import com.hippo.ehviewer.util.FileUtils
|
||||
import eu.kanade.tachiyomi.ui.reader.loader.PageLoader
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.CoroutineStart
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
import okio.Path
|
||||
|
||||
abstract class PageLoader2(private val gid: Long, var startPage: Int) : PageLoader(), CoroutineScope {
|
||||
abstract class PageLoader2(private val gid: Long, var startPage: Int) :
|
||||
PageLoader(),
|
||||
CoroutineScope {
|
||||
override val coroutineContext = Dispatchers.IO + Job()
|
||||
private val progressJob = launch(start = CoroutineStart.LAZY) {
|
||||
private val progressJob = async(start = CoroutineStart.LAZY) {
|
||||
if (startPage == -1) {
|
||||
startPage = EhDB.getReadProgress(gid)
|
||||
}
|
||||
}
|
||||
|
||||
override fun start() {
|
||||
super.start()
|
||||
progressJob.start()
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
override suspend fun awaitReady(): Boolean {
|
||||
progressJob.join()
|
||||
return startPage != -1
|
||||
progressJob.await()
|
||||
return super.awaitReady() && startPage != -1
|
||||
}
|
||||
|
||||
override fun stop() {
|
||||
@@ -59,16 +60,11 @@ abstract class PageLoader2(private val gid: Long, var startPage: Int) : PageLoad
|
||||
|
||||
protected abstract val title: String
|
||||
|
||||
protected abstract fun getImageExtension(index: Int): String
|
||||
protected abstract fun getImageExtension(index: Int): String?
|
||||
|
||||
fun getImageFilename(index: Int): String {
|
||||
return "$title - ${index + 1}.${getImageExtension(index)}"
|
||||
fun getImageFilename(index: Int): String? = getImageExtension(index)?.let {
|
||||
FileUtils.sanitizeFilename("$title - ${index + 1}.${it.lowercase()}")
|
||||
}
|
||||
|
||||
abstract fun save(index: Int, file: UniFile): Boolean
|
||||
|
||||
fun saveToDir(index: Int, dir: UniFile): UniFile? {
|
||||
val filename = getImageFilename(index)
|
||||
return (dir / filename).takeIf { save(index, it) }
|
||||
}
|
||||
abstract fun save(index: Int, file: Path): Boolean
|
||||
}
|
||||
@@ -18,8 +18,8 @@
|
||||
package com.hippo.ehviewer.image
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.drawable.Animatable
|
||||
import androidx.compose.ui.unit.IntRect
|
||||
import android.hardware.HardwareBuffer
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import arrow.core.Either
|
||||
import arrow.core.left
|
||||
import arrow.core.right
|
||||
@@ -30,10 +30,13 @@ import coil3.imageLoader
|
||||
import coil3.request.CachePolicy
|
||||
import coil3.request.ErrorResult
|
||||
import coil3.request.SuccessResult
|
||||
import coil3.request.allowHardware
|
||||
import coil3.size.Dimension
|
||||
import coil3.size.Precision
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.coil.BitmapImageWithRect
|
||||
import com.hippo.ehviewer.coil.BitmapImageWithExtraInfo
|
||||
import com.hippo.ehviewer.coil.detectQrCode
|
||||
import com.hippo.ehviewer.coil.hardwareThreshold
|
||||
import com.hippo.ehviewer.coil.maybeCropBorder
|
||||
import com.hippo.ehviewer.jni.isGif
|
||||
import com.hippo.ehviewer.jni.mmap
|
||||
@@ -42,31 +45,32 @@ import com.hippo.ehviewer.jni.rewriteGifSource
|
||||
import com.hippo.ehviewer.ktbuilder.imageRequest
|
||||
import com.hippo.ehviewer.util.isAtLeastP
|
||||
import com.hippo.ehviewer.util.isAtLeastU
|
||||
import com.hippo.unifile.UniFile
|
||||
import com.hippo.files.openFileDescriptor
|
||||
import com.hippo.files.toUri
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import java.nio.ByteBuffer
|
||||
import okio.Path
|
||||
import splitties.init.appCtx
|
||||
|
||||
class Image private constructor(image: CoilImage, private val src: AutoCloseable) {
|
||||
val size = image.size
|
||||
val rect = when (image) {
|
||||
is BitmapImageWithRect -> image.rect
|
||||
else -> image.run { IntRect(0, 0, width, height) }
|
||||
class Image private constructor(image: CoilImage, private val src: ImageSource) {
|
||||
val intrinsicSize = with(image) { IntSize(width, height) }
|
||||
val allocationSize = image.size
|
||||
val hasQrCode = when (image) {
|
||||
is BitmapImageWithExtraInfo -> image.hasQrCode
|
||||
else -> false
|
||||
}
|
||||
|
||||
var innerImage: CoilImage? = when (image) {
|
||||
is BitmapImageWithRect -> image.image
|
||||
is BitmapImageWithExtraInfo -> image.image
|
||||
else -> image
|
||||
}
|
||||
|
||||
var isRecyclable = true
|
||||
|
||||
@Synchronized
|
||||
fun recycle() {
|
||||
when (val image = innerImage ?: return) {
|
||||
is DrawableImage -> {
|
||||
(image.drawable as Animatable).stop()
|
||||
image.drawable.callback = null
|
||||
src.close()
|
||||
}
|
||||
is DrawableImage -> src.close()
|
||||
is BitmapImage -> image.bitmap.recycle()
|
||||
}
|
||||
innerImage = null
|
||||
@@ -75,13 +79,16 @@ class Image private constructor(image: CoilImage, private val src: AutoCloseable
|
||||
companion object {
|
||||
private val targetWidth = appCtx.resources.displayMetrics.widthPixels * 3
|
||||
|
||||
private suspend fun Either<ByteBufferSource, UniFileSource>.decodeCoil(): CoilImage {
|
||||
private suspend fun Either<ByteBufferSource, PathSource>.decodeCoil(checkExtraneousAds: Boolean): CoilImage {
|
||||
val req = appCtx.imageRequest {
|
||||
onLeft { data(it.source) }
|
||||
onRight { data(it.source.uri) }
|
||||
onRight { data(it.source.toUri()) }
|
||||
size(Dimension(targetWidth), Dimension.Undefined)
|
||||
precision(Precision.INEXACT)
|
||||
allowHardware(false)
|
||||
hardwareThreshold(Settings.hardwareBitmapThreshold)
|
||||
maybeCropBorder(Settings.cropBorder.value)
|
||||
detectQrCode(checkExtraneousAds)
|
||||
memoryCachePolicy(CachePolicy.DISABLED)
|
||||
}
|
||||
return when (val result = appCtx.imageLoader.execute(req)) {
|
||||
@@ -90,10 +97,10 @@ class Image private constructor(image: CoilImage, private val src: AutoCloseable
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun decode(src: ImageSource): Image? {
|
||||
suspend fun decode(src: ImageSource, checkExtraneousAds: Boolean = false): Image? {
|
||||
return runCatching {
|
||||
val image = when (src) {
|
||||
is UniFileSource -> {
|
||||
is PathSource -> {
|
||||
if (isAtLeastP && !isAtLeastU) {
|
||||
src.source.openFileDescriptor("rw").use {
|
||||
val fd = it.fd
|
||||
@@ -106,18 +113,18 @@ class Image private constructor(image: CoilImage, private val src: AutoCloseable
|
||||
src.close()
|
||||
}
|
||||
}
|
||||
return decode(source)
|
||||
return decode(source, checkExtraneousAds)
|
||||
}
|
||||
}
|
||||
}
|
||||
src.right().decodeCoil()
|
||||
src.right().decodeCoil(checkExtraneousAds)
|
||||
}
|
||||
|
||||
is ByteBufferSource -> {
|
||||
if (isAtLeastP && !isAtLeastU) {
|
||||
rewriteGifSource(src.source)
|
||||
}
|
||||
src.left().decodeCoil()
|
||||
src.left().decodeCoil(checkExtraneousAds)
|
||||
}
|
||||
}
|
||||
Image(image, src).apply {
|
||||
@@ -133,8 +140,8 @@ class Image private constructor(image: CoilImage, private val src: AutoCloseable
|
||||
|
||||
sealed interface ImageSource : AutoCloseable
|
||||
|
||||
interface UniFileSource : ImageSource {
|
||||
val source: UniFile
|
||||
interface PathSource : ImageSource {
|
||||
val source: Path
|
||||
val type: String
|
||||
}
|
||||
|
||||
@@ -143,3 +150,5 @@ interface ByteBufferSource : ImageSource {
|
||||
}
|
||||
|
||||
external fun detectBorder(bitmap: Bitmap): IntArray
|
||||
external fun hasQrCode(bitmap: Bitmap): Boolean
|
||||
external fun copyBitmapToAHB(src: Bitmap, dst: HardwareBuffer, x: Int, y: Int)
|
||||
@@ -10,4 +10,4 @@ external fun getExtension(index: Int): String
|
||||
external fun needPassword(): Boolean
|
||||
external fun providePassword(str: String): Boolean
|
||||
external fun closeArchive()
|
||||
external fun archiveFdBatch(fdBatch: IntArray, names: Array<String>, arcFd: Int, size: Int): Boolean
|
||||
external fun archiveFdBatch(fdBatch: IntArray, names: Array<String>, arcFd: Int, size: Int)
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.hippo.ehviewer.ktor
|
||||
|
||||
import io.ktor.client.engine.HttpClientEngine
|
||||
import io.ktor.client.engine.HttpClientEngineFactory
|
||||
|
||||
object Cronet : HttpClientEngineFactory<CronetConfig> {
|
||||
override fun create(block: CronetConfig.() -> Unit): HttpClientEngine =
|
||||
CronetEngine(CronetConfig().apply(block))
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.hippo.ehviewer.ktor
|
||||
|
||||
import android.content.Context
|
||||
import io.ktor.client.engine.HttpClientEngineConfig
|
||||
import org.chromium.net.CronetEngine
|
||||
|
||||
class CronetConfig : HttpClientEngineConfig() {
|
||||
var client: CronetEngine? = null
|
||||
|
||||
fun config(context: Context, block: CronetEngine.Builder.() -> Unit) {
|
||||
client = CronetEngine.Builder(context).apply(block).build()
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,6 @@
|
||||
package com.hippo.ehviewer.ktor
|
||||
|
||||
import com.hippo.ehviewer.cronet.cronetDispatcher
|
||||
import com.hippo.ehviewer.cronet.cronetHttpClient
|
||||
import com.hippo.ehviewer.cronet.cronetHttpClientExecutor
|
||||
import com.hippo.ehviewer.cronet.pool
|
||||
import io.ktor.client.engine.HttpClientEngineBase
|
||||
import io.ktor.client.engine.HttpClientEngineConfig
|
||||
import io.ktor.client.engine.callContext
|
||||
import io.ktor.client.request.HttpRequestData
|
||||
import io.ktor.client.request.HttpResponseData
|
||||
@@ -18,12 +13,16 @@ import io.ktor.util.date.GMTDate
|
||||
import io.ktor.util.flattenForEach
|
||||
import io.ktor.utils.io.ByteReadChannel
|
||||
import io.ktor.utils.io.InternalAPI
|
||||
import io.ktor.utils.io.pool.DirectByteBufferPool
|
||||
import io.ktor.utils.io.pool.useInstance
|
||||
import io.ktor.utils.io.writeFully
|
||||
import io.ktor.utils.io.writer
|
||||
import java.nio.ByteBuffer
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.asExecutor
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.channels.consumeEach
|
||||
import kotlinx.coroutines.job
|
||||
@@ -33,9 +32,12 @@ import org.chromium.net.UrlRequest
|
||||
import org.chromium.net.UrlResponseInfo
|
||||
import org.chromium.net.apihelpers.UploadDataProviders
|
||||
|
||||
object CronetEngine : HttpClientEngineBase("Cronet") {
|
||||
override val config = HttpClientEngineConfig()
|
||||
override val dispatcher = cronetDispatcher
|
||||
class CronetEngine(override val config: CronetConfig) : HttpClientEngineBase("Cronet") {
|
||||
// Limit thread to 1 since we are async & non-blocking
|
||||
override val dispatcher = Dispatchers.Default.limitedParallelism(1)
|
||||
private val executor = dispatcher.asExecutor()
|
||||
private val pool = DirectByteBufferPool(32)
|
||||
private val client = config.client ?: error("Cronet client is not configured")
|
||||
|
||||
@InternalAPI
|
||||
override suspend fun execute(data: HttpRequestData) = executeHttpRequest(callContext(), data)
|
||||
@@ -94,12 +96,12 @@ object CronetEngine : HttpClientEngineBase("Cronet") {
|
||||
}
|
||||
}
|
||||
|
||||
cronetHttpClient.newUrlRequestBuilder(data.url.toString(), callback, cronetHttpClientExecutor).apply {
|
||||
client.newUrlRequestBuilder(data.url.toString(), callback, executor).apply {
|
||||
setHttpMethod(data.method.value)
|
||||
data.headers.flattenForEach { key, value -> addHeader(key, value) }
|
||||
data.body.contentType?.let { addHeader(HttpHeaders.ContentType, it.toString()) }
|
||||
data.body.contentLength?.let { addHeader(HttpHeaders.ContentLength, it.toString()) }
|
||||
data.body.toUploadDataProvider()?.let { setUploadDataProvider(it, cronetHttpClientExecutor) }
|
||||
data.body.toUploadDataProvider()?.let { setUploadDataProvider(it, executor) }
|
||||
}.build().apply {
|
||||
start()
|
||||
callContext.job.invokeOnCompletion { cancel() }
|
||||
|
||||
@@ -7,17 +7,11 @@ import okio.source
|
||||
|
||||
fun readLegacySpiderInfo(inputStream: InputStream): SpiderInfo {
|
||||
val source = inputStream.source().buffer()
|
||||
fun read(): String {
|
||||
return source.readUtf8LineStrict()
|
||||
}
|
||||
fun read(): String = source.readUtf8LineStrict()
|
||||
|
||||
fun readInt(): Int {
|
||||
return read().toInt()
|
||||
}
|
||||
fun readInt(): Int = read().toInt()
|
||||
|
||||
fun readLong(): Long {
|
||||
return read().toLong()
|
||||
}
|
||||
fun readLong(): Long = read().toLong()
|
||||
repeat(2) { read() } // We assert that only info v2
|
||||
val gid = readLong()
|
||||
val token = read()
|
||||
|
||||
@@ -5,11 +5,8 @@ import com.hippo.ehviewer.client.data.GalleryDetail
|
||||
import com.hippo.ehviewer.client.data.GalleryInfo
|
||||
import com.hippo.ehviewer.client.data.SimpleTagsConverter
|
||||
import com.hippo.ehviewer.client.data.TagNamespace
|
||||
import com.hippo.unifile.UniFile
|
||||
import com.hippo.unifile.openInputStream
|
||||
import com.hippo.unifile.openOutputStream
|
||||
import java.io.InputStreamReader
|
||||
import java.io.OutputStreamWriter
|
||||
import com.hippo.files.openInputStream
|
||||
import com.hippo.files.openOutputStream
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
@@ -19,10 +16,11 @@ import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
import nl.adaptivity.xmlutil.XmlDeclMode
|
||||
import nl.adaptivity.xmlutil.core.XmlVersion
|
||||
import nl.adaptivity.xmlutil.newGenericWriter
|
||||
import nl.adaptivity.xmlutil.newWriter
|
||||
import nl.adaptivity.xmlutil.serialization.XML
|
||||
import nl.adaptivity.xmlutil.serialization.XmlElement
|
||||
import nl.adaptivity.xmlutil.xmlStreaming
|
||||
import okio.Path
|
||||
|
||||
const val COMIC_INFO_FILE = "ComicInfo.xml"
|
||||
private const val TAG_ORIGINAL = "original"
|
||||
@@ -62,6 +60,7 @@ fun GalleryInfo.getComicInfo(): ComicInfo {
|
||||
|
||||
else -> simpleTags?.forEach { tagString ->
|
||||
val (namespace, tag) = tagString.split(':', limit = 2)
|
||||
.takeIf { it.size == 2 } ?: return@forEach // Ignore temp tags that don't have namespace
|
||||
when (val ns = TagNamespace(namespace)) {
|
||||
Artist, Cosplayer -> artists.add(tag)
|
||||
Group -> groups.add(tag)
|
||||
@@ -76,7 +75,7 @@ fun GalleryInfo.getComicInfo(): ComicInfo {
|
||||
}
|
||||
return ComicInfo(
|
||||
series = title,
|
||||
alternateSeries = titleJpn.takeUnless { it.isNullOrBlank() },
|
||||
alternateSeries = titleJpn?.ifBlank { null },
|
||||
writer = groups.ifEmpty { null },
|
||||
penciller = artists.ifEmpty { null },
|
||||
genre = otherTags.ifEmpty { null },
|
||||
@@ -97,22 +96,18 @@ fun ComicInfo.toSimpleTags() = listOfNotNull(
|
||||
teams,
|
||||
).flatten().ifEmpty { null }
|
||||
|
||||
fun ComicInfo.write(file: UniFile) {
|
||||
file.openOutputStream().use {
|
||||
OutputStreamWriter(it, Charsets.UTF_8).use { writer ->
|
||||
xmlStreaming.newGenericWriter(writer).use { xmlWriter ->
|
||||
xml.encodeToWriter(xmlWriter, ComicInfo.serializer(), this)
|
||||
}
|
||||
fun ComicInfo.write(file: Path) {
|
||||
file.openOutputStream().bufferedWriter().use {
|
||||
xmlStreaming.newWriter(it).use { writer ->
|
||||
xml.encodeToWriter(writer, ComicInfo.serializer(), this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun readComicInfo(file: UniFile): ComicInfo? = runCatching {
|
||||
file.openInputStream().use {
|
||||
InputStreamReader(it, Charsets.UTF_8).use { reader ->
|
||||
xmlStreaming.newGenericReader(reader).use { xmlReader ->
|
||||
xml.decodeFromReader(ComicInfo.serializer(), xmlReader)
|
||||
}
|
||||
fun readComicInfo(file: Path): ComicInfo? = runCatching {
|
||||
file.openInputStream().bufferedReader().use {
|
||||
xmlStreaming.newReader(it).use { reader ->
|
||||
xml.decodeFromReader(ComicInfo.serializer(), reader)
|
||||
}
|
||||
}
|
||||
}.getOrNull()
|
||||
|
||||
Loaded 100 of 326 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user