Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
a3cb6d2782 | ||
|
|
45be957df5 | ||
|
|
8e45014e3f | ||
|
|
d84e948352 | ||
|
|
68bd835038 | ||
|
|
c297bebea1 | ||
|
|
6c0a823216 | ||
|
|
067a701bdc | ||
|
|
c3fb008c92 | ||
|
|
bc4321da2e | ||
|
|
8dec040803 | ||
|
|
29e4e77c44 | ||
|
|
11429ef7fe | ||
|
|
f5fcd96a1e | ||
|
|
19120038c5 | ||
|
|
9021f28ead | ||
|
|
62934f72e3 | ||
|
|
6c534e1049 | ||
|
|
f80cd90792 | ||
|
|
3999c0c114 | ||
|
|
59399453c4 | ||
|
|
694bb19e2a | ||
|
|
9928ecd4d9 | ||
|
|
2c5293af6e | ||
|
|
9569ca774d | ||
|
|
3a04460926 | ||
|
|
acb4fb6166 | ||
|
|
7d137bb016 | ||
|
|
8c2ed37b72 | ||
|
|
749b230250 | ||
|
|
23531cb6e0 | ||
|
|
0ac11dbe0b | ||
|
|
9b0e017542 | ||
|
|
f2f5fcacf9 | ||
|
|
52ded265f3 | ||
|
|
e77f905c4a | ||
|
|
2d14d34d53 | ||
|
|
78facda18a | ||
|
|
1fe1b43015 | ||
|
|
63164dc70a | ||
|
|
0d39092a7f | ||
|
|
c50db8a327 | ||
|
|
7e107cc5e2 | ||
|
|
260949e3a7 | ||
|
|
d9ec108721 | ||
|
|
1e97407936 | ||
|
|
73afe05a18 | ||
|
|
23475de932 | ||
|
|
6fb2172c2c | ||
|
|
9226b71119 | ||
|
|
58e38b58bf | ||
|
|
db0d110627 | ||
|
|
08812bf7ff | ||
|
|
daf7f8ffca | ||
|
|
74564fa07f | ||
|
|
fc51d3fc81 | ||
|
|
c5d5b8abd2 | ||
|
|
193badccb8 | ||
|
|
2653d0568a | ||
|
|
adf5a65371 | ||
|
|
5b59dabc66 | ||
|
|
f7259c5b40 | ||
|
|
6f9fceaef8 | ||
|
|
6a64ff51ed | ||
|
|
ea9d9cfba7 | ||
|
|
75c6561896 | ||
|
|
8f8db82e9d | ||
|
|
a114a8ac72 | ||
|
|
2c6216057f | ||
|
|
c5cbcf7418 | ||
|
|
5746e3bb57 | ||
|
|
fd3c87be22 | ||
|
|
866fff3e45 | ||
|
|
06df7befb6 | ||
|
|
8021f86473 | ||
|
|
c9919a8b39 | ||
|
|
386015a9ec | ||
|
|
cbce98b34e | ||
|
|
f69521fc1a | ||
|
|
01b4054368 | ||
|
|
e07aab4434 | ||
|
|
18caa87512 | ||
|
|
5676de24da | ||
|
|
57bbb1fe38 | ||
|
|
dcbfbc2e5f | ||
|
|
e1f473d301 | ||
|
|
a33256d29f | ||
|
|
6a81877b09 | ||
|
|
6cd7b85b94 | ||
|
|
df97a9c4b9 | ||
|
|
5cb91e9b45 | ||
|
|
94794cb0db | ||
|
|
a16c021000 | ||
|
|
49d4ae078f | ||
|
|
eb45068df3 | ||
|
|
1aa750d676 | ||
|
|
af85a68e3d | ||
|
|
a8fc4003a3 | ||
|
|
675df44745 | ||
|
|
7e271d7e8e | ||
|
|
acb2c720af | ||
|
|
5d0c2e3f05 | ||
|
|
3d5e3620dd | ||
|
|
43aefeedae | ||
|
|
c2cbcc009d | ||
|
|
2aa494d9b7 | ||
|
|
8f3a11f374 | ||
|
|
76298f15e4 | ||
|
|
58839e0def | ||
|
|
6fe62db5e4 | ||
|
|
9b9f25d405 | ||
|
|
e47d7378ec | ||
|
|
51b385408d | ||
|
|
8e7cced8d2 | ||
|
|
affeb3e106 | ||
|
|
a2377857e9 | ||
|
|
7714ff8938 | ||
|
|
352853b88c | ||
|
|
23d282e354 | ||
|
|
b93c12de89 | ||
|
|
e0e5a155aa | ||
|
|
e1f34a7b1b | ||
|
|
298e80eb9d | ||
|
|
3ecfd83f1e | ||
|
|
1dcf4e7f46 | ||
|
|
f1a856cb45 | ||
|
|
af32d7eea7 | ||
|
|
47be2754e5 | ||
|
|
39b16b6650 | ||
|
|
0a809a564c | ||
|
|
07e24013c8 | ||
|
|
8e8394985d | ||
|
|
e54b33aaaa | ||
|
|
e6a22d8ef4 | ||
|
|
ae25018574 | ||
|
|
28160608ec | ||
|
|
38651eba1d | ||
|
|
cc53fa5f1c | ||
|
|
70f8181752 | ||
|
|
9e506f6258 | ||
|
|
1304c728cf | ||
|
|
80e11c54e7 | ||
|
|
ff5d58b101 | ||
|
|
bf550d99bc | ||
|
|
7738b7bc29 | ||
|
|
01f51b1000 | ||
|
|
16dfacb285 | ||
|
|
a253ddf7e4 | ||
|
|
ffb488482c | ||
|
|
0e29754ce9 | ||
|
|
ee97720e1f | ||
|
|
29d4e6370c | ||
|
|
9040cb54d4 | ||
|
|
f082766f06 | ||
|
|
e2372891cb | ||
|
|
9dc5b5c539 | ||
|
|
886e025ac8 | ||
|
|
feb13cbd79 | ||
|
|
66baaad86f | ||
|
|
4142f28822 | ||
|
|
c578b16e27 | ||
|
|
32887acecd | ||
|
|
5cf5cc5e34 | ||
|
|
4dc0f7e02e | ||
|
|
6e9c6e6269 | ||
|
|
a22880a9b1 | ||
|
|
7357724392 | ||
|
|
fb1b08e7e8 | ||
|
|
1405b99ab8 | ||
|
|
f7aec69dae | ||
|
|
f15a0fc4bb | ||
|
|
6330fc3bdc | ||
|
|
01e3f3e9b7 | ||
|
|
e0c614cf71 | ||
|
|
b34764d4c2 | ||
|
|
f46f84e533 | ||
|
|
ca0eaf2053 | ||
|
|
25f283ee9c | ||
|
|
8ee23ab520 | ||
|
|
6964baf3f6 | ||
|
|
01aa7af175 | ||
|
|
5982bd2d4b | ||
|
|
d0fbb3539f | ||
|
|
284fb4242b | ||
|
|
beeb4bdbd4 | ||
|
|
76a52903e5 | ||
|
|
a19b8782a8 | ||
|
|
31f66fd05b | ||
|
|
bc3c4e969c | ||
|
|
4231402e54 | ||
|
|
ff4f76dfa5 | ||
|
|
2ff9df4c07 | ||
|
|
7cbc641178 | ||
|
|
08d5621b59 | ||
|
|
d39c2fdfa2 | ||
|
|
27e02ad642 | ||
|
|
11e9faef7c | ||
|
|
cbb8971dd1 | ||
|
|
1077f4d8cf | ||
|
|
594f9412e3 | ||
|
|
7674f6a0f7 | ||
|
|
aea6fbf4ab | ||
|
|
962f9bfff5 | ||
|
|
60532e2622 | ||
|
|
5c7a834342 | ||
|
|
ca940080a9 | ||
|
|
2273dd541a | ||
|
|
422062226d | ||
|
|
6a6da6f059 | ||
|
|
4cbb0eca4f | ||
|
|
ebb3eb1ab3 | ||
|
|
ebf645acaf | ||
|
|
ec787ebde2 | ||
|
|
4f190cb0ef | ||
|
|
2a36868d11 | ||
|
|
176d8be565 | ||
|
|
7b581e45e1 | ||
|
|
4888e29f79 | ||
|
|
e5f6d1b1aa | ||
|
|
d544bbfbeb | ||
|
|
ed20d6022b | ||
|
|
4191e2091c | ||
|
|
1eeb5a51f6 | ||
|
|
dd0776758f | ||
|
|
cf3ccf6155 | ||
|
|
fa6e4acad6 | ||
|
|
db33a72a8e | ||
|
|
0f8a0549b1 | ||
|
|
26ff30af6c | ||
|
|
c2fa32f430 | ||
|
|
2734de0588 | ||
|
|
aa219e4b71 | ||
|
|
645656b4cd | ||
|
|
ef16bdeb75 | ||
|
|
84299b4db7 | ||
|
|
953e216dde | ||
|
|
c57fd0a923 | ||
|
|
f9cb3d769f | ||
|
|
dd62571943 | ||
|
|
9edd33d88d | ||
|
|
b2fded76d5 | ||
|
|
1761493c8e | ||
|
|
6aa6ee2810 | ||
|
|
84728a1490 | ||
|
|
a1568467f0 | ||
|
|
32e2a03c31 | ||
|
|
d4609593e2 | ||
|
|
a2af7437c0 | ||
|
|
ab5af22fc3 | ||
|
|
39a159e5ca | ||
|
|
ec8a54906c | ||
|
|
73c0ce02ea | ||
|
|
7d2e0f7f7e | ||
|
|
7515a049c2 | ||
|
|
a317d5303e | ||
|
|
c4045ded70 | ||
|
|
3cb1b1eee4 | ||
|
|
5423af46c5 | ||
|
|
a2883840e0 | ||
|
|
14810b4c2c | ||
|
|
f5a9a41790 | ||
|
|
500d00d214 | ||
|
|
49810f8fd1 | ||
|
|
6ad3ab228f | ||
|
|
3a29ce291b | ||
|
|
1e28a4c7ab | ||
|
|
645467d20f | ||
|
|
b2a5fe9f52 | ||
|
|
2ea3c03360 | ||
|
|
0c147967d2 | ||
|
|
de1fab2595 | ||
|
|
b86eebf0a7 | ||
|
|
07345f90e6 | ||
|
|
5fe520e575 | ||
|
|
a9dd705ec5 | ||
|
|
e36520faad | ||
|
|
edaf0a4275 | ||
|
|
7735237ea7 | ||
|
|
7bbc4ea618 | ||
|
|
db99392082 | ||
|
|
7bf6a9f545 | ||
|
|
51c0efebc4 | ||
|
|
7ef70aa16e | ||
|
|
c5c0c6c4bd | ||
|
|
b26cba8ea9 | ||
|
|
90b4c16166 | ||
|
|
9b0f6fe433 | ||
|
|
e24735c341 | ||
|
|
9680ada9ee | ||
|
|
028a7c2352 | ||
|
|
80e5f36c4e | ||
|
|
e0b65184e2 | ||
|
|
395a48ddeb | ||
|
|
e22a39f549 | ||
|
|
e17d934ebf | ||
|
|
10746417b6 | ||
|
|
3ef45ba0cc | ||
|
|
a3e7b6ef43 | ||
|
|
c444a3b435 | ||
|
|
5161802465 | ||
|
|
8c27feec7a | ||
|
|
37e895cdca | ||
|
|
e1ad4e3b90 | ||
|
|
5f0be5ae43 | ||
|
|
9a7cabb221 | ||
|
|
6acc552058 | ||
|
|
b4f4d442eb | ||
|
|
21016dda53 | ||
|
|
576e5b1b58 | ||
|
|
17437a6882 | ||
|
|
04f9f087da | ||
|
|
c508947728 | ||
|
|
ce1dc55d3c | ||
|
|
e9f615819f | ||
|
|
8da3546558 | ||
|
|
d8f8dcffbd | ||
|
|
e79113b19e | ||
|
|
2a7f65a1bd | ||
|
|
e322878b95 | ||
|
|
11b09f187c | ||
|
|
926309733c | ||
|
|
c4b2bcab66 | ||
|
|
b88c96894d | ||
|
|
8502671203 | ||
|
|
4a7d6f7489 | ||
|
|
aec34c81aa | ||
|
|
1d4fe30e04 | ||
|
|
a2cfecd922 | ||
|
|
d36aef3214 | ||
|
|
c632231813 | ||
|
|
d8dd68c9b6 | ||
|
|
d6a58d5d34 | ||
|
|
41f8168c28 | ||
|
|
4513ead30a | ||
|
|
f4e23a5738 | ||
|
|
79b167002c | ||
|
|
a9a763ae5f | ||
|
|
a5fc29bc66 | ||
|
|
02c568fd7a | ||
|
|
ce55891bc2 | ||
|
|
87b1160c72 | ||
|
|
ccc6d004de | ||
|
|
20b145bee5 | ||
|
|
a37cc59a56 | ||
|
|
7cbffff12c | ||
|
|
4e5ae53775 | ||
|
|
a53de63695 | ||
|
|
8020c139ec | ||
|
|
3c5375a9a6 | ||
|
|
1d9008a1a6 | ||
|
|
fb577dc026 | ||
|
|
675eb1b7e2 | ||
|
|
6a3d522dfa | ||
|
|
47aa5c64e7 | ||
|
|
5ecf5c41ca | ||
|
|
bc6d12a873 | ||
|
|
4d983830df | ||
|
|
2f1e0d030e | ||
|
|
b7c873a172 | ||
|
|
9d5eb55290 | ||
|
|
78ffb1c4fe | ||
|
|
031d622d5d | ||
|
|
34da120a43 | ||
|
|
5285e1ccf2 | ||
|
|
e28efc1f50 | ||
|
|
1c043a81a6 | ||
|
|
9a769cc7b6 | ||
|
|
045c6abe40 | ||
|
|
4ad5cef412 | ||
|
|
c3ffab3c79 | ||
|
|
c82de0b779 | ||
|
|
4e36cbabc0 | ||
|
|
94f87b14b7 | ||
|
|
6bc80d1afa | ||
|
|
fe4d004083 | ||
|
|
28410befa3 | ||
|
|
515f7deb7e | ||
|
|
800e66261e | ||
|
|
65658c4a02 | ||
|
|
0488c7ebf4 | ||
|
|
c30c1b29ef | ||
|
|
876001ad99 | ||
|
|
60dc59adbc | ||
|
|
c994890f28 | ||
|
|
76223a2cd2 | ||
|
|
6274b39b68 | ||
|
|
cca3a3e350 | ||
|
|
86ef8f2b48 | ||
|
|
5d116d9477 | ||
|
|
6e73fcc8c9 | ||
|
|
3bd12a3047 | ||
|
|
c99d8e8275 | ||
|
|
4a12e71877 | ||
|
|
3043901b5a | ||
|
|
7cc28955d6 | ||
|
|
ef44bb4857 | ||
|
|
37660ef697 | ||
|
|
9821b3aaab | ||
|
|
1f5331af5a | ||
|
|
3ea6c82a6d | ||
|
|
ba53d01fa1 | ||
|
|
df8ae8d11a | ||
|
|
c817726ab3 | ||
|
|
9eb606810f | ||
|
|
fbb030ec8e | ||
|
|
28afebefb1 | ||
|
|
da2610657c | ||
|
|
6e3471ba1b | ||
|
|
5a9ab6c4eb | ||
|
|
1fe553d039 | ||
|
|
42e0b0aeab | ||
|
|
9e0ea32296 | ||
|
|
d3f5274b7e | ||
|
|
faff0733e9 | ||
|
|
7b32cde7dd | ||
|
|
648d1d10b3 | ||
|
|
52727100a0 | ||
|
|
8a2dbb99f0 | ||
|
|
d880b30ea9 | ||
|
|
0e2ca4e150 | ||
|
|
e0eef82ed8 | ||
|
|
09839086a2 | ||
|
|
3ca3ab7273 | ||
|
|
8386bf9c69 | ||
|
|
99b8ee1f78 | ||
|
|
0333a8c679 | ||
|
|
e0bb09e902 | ||
|
|
77677cead3 | ||
|
|
d4b2ff7de9 | ||
|
|
b81bae09d1 | ||
|
|
846e66a56c | ||
|
|
5cec635e52 | ||
|
|
8d0dc88125 | ||
|
|
51b58bb797 | ||
|
|
f79aa2350c | ||
|
|
719dddeab9 | ||
|
|
c1522568fd | ||
|
|
3ad6b9106b | ||
|
|
549387ac91 | ||
|
|
7d2f985ffa | ||
|
|
7e8b0a5800 | ||
|
|
69252ae8a4 | ||
|
|
eeed56b211 | ||
|
|
8aaad071ea | ||
|
|
7ef67e1bf9 | ||
|
|
1c15d9a956 | ||
|
|
70082090a1 | ||
|
|
dbc1a32496 | ||
|
|
e8c9ee5dd0 | ||
|
|
5c3657736b | ||
|
|
5191408d06 | ||
|
|
347ce6854d | ||
|
|
f86dec9b2e | ||
|
|
c74b128b1a | ||
|
|
8ee28a4b4f | ||
|
|
3b07b6997d | ||
|
|
f59023ed03 | ||
|
|
3fc13e61c3 | ||
|
|
ae22f448a8 | ||
|
|
450c15d51e | ||
|
|
d9e5bb4399 | ||
|
|
369904f659 | ||
|
|
96302b2344 | ||
|
|
eeffb12e3b | ||
|
|
57c12f7df0 |
No files matched your search
@@ -0,0 +1 @@
|
||||
*.sh eol=lf
|
||||
@@ -141,9 +141,6 @@ body:
|
||||
请确认您已经遵守所有必选项
|
||||
Please ensure you have obtained all needed options
|
||||
options:
|
||||
- label: 如果您有足够的时间和能力,并愿意为此提交 PR ,请勾上此复选框 / Pull request is welcome. Check this if you want to start a pull request
|
||||
required: false
|
||||
|
||||
- label: 您已搜索过 [Issue Tracker](https://github.com/FooIbar/EhViewer/issues),没有找到类似内容 / I have searched on [Issue Tracker](https://github.com/FooIbar/EhViewer/issues), No duplicate or related open issue has been found
|
||||
required: true
|
||||
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
name: Baseline profile generation
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '30 0 * * 0'
|
||||
workflow_dispatch:
|
||||
push:
|
||||
paths:
|
||||
- 'benchmark/**'
|
||||
- '.github/workflows/baseline-profile.yml'
|
||||
|
||||
jobs:
|
||||
baseline-profile:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 21
|
||||
|
||||
- name: Free disk space
|
||||
run: rm -rf ${ANDROID_SDK_ROOT}/ndk
|
||||
|
||||
- name: Install NDK
|
||||
run: echo "y" | ${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager --install "ndk;27.0.11902837"
|
||||
|
||||
- name: Install Rust Toolchain
|
||||
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
|
||||
with:
|
||||
workspaces: "./app/src/main/rust"
|
||||
shared-key: ${{ runner.os }}
|
||||
save-if: "false"
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v3
|
||||
with:
|
||||
cache-encryption-key: ${{ secrets.GradleEncryptionKey }}
|
||||
cache-read-only: true
|
||||
|
||||
- name: CMake Cache
|
||||
uses: actions/cache/restore@v4
|
||||
with:
|
||||
path: app/.cxx
|
||||
key: cmake-
|
||||
|
||||
- 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: Build
|
||||
run: ./gradlew generateBaselineProfile
|
||||
-Pandroid.testoptions.manageddevices.emulator.gpu="swiftshader_indirect"
|
||||
|
||||
- name: Update baseline profile
|
||||
if: github.ref == 'refs/heads/main'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
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/baseline-profile
|
||||
if git commit -am "Update baseline profile"; then
|
||||
git push
|
||||
gh pr create -f
|
||||
fi
|
||||
|
||||
- name: Upload reports
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: reports
|
||||
path: benchmark/build/reports
|
||||
+127
-142
@@ -8,28 +8,26 @@ on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
linux:
|
||||
name: Build
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
||||
with:
|
||||
submodules: 'recursive'
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Java 17
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 17
|
||||
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
|
||||
with:
|
||||
workspaces: "./app/src/main/rust"
|
||||
shared-key: ${{ runner.os }}
|
||||
|
||||
- name: Rustfmt Check
|
||||
working-directory: ./app/src/main/rust
|
||||
@@ -40,174 +38,161 @@ jobs:
|
||||
run: cargo clippy --all-targets --all-features -- -D warnings
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/gradle-build-action@v2
|
||||
uses: gradle/actions/setup-gradle@v3
|
||||
with:
|
||||
gradle-home-cache-cleanup: true
|
||||
cache-read-only: true
|
||||
|
||||
- name: Install NDK
|
||||
run: echo "y" | ${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager --install "ndk;27.0.11902837"
|
||||
|
||||
- name: Spotless Check
|
||||
run: ./gradlew spotlessCheck
|
||||
|
||||
- name: Lint Check
|
||||
run: ./gradlew lintMarshmallowRelease
|
||||
|
||||
default:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 21
|
||||
|
||||
- name: Install NDK
|
||||
run: echo "y" | ${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager --install "ndk;27.0.11902837"
|
||||
|
||||
- name: Install Rust Toolchain
|
||||
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
|
||||
with:
|
||||
workspaces: "./app/src/main/rust"
|
||||
shared-key: ${{ runner.os }}
|
||||
save-if: "false"
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v3
|
||||
with:
|
||||
cache-encryption-key: ${{ secrets.GradleEncryptionKey }}
|
||||
gradle-home-cache-cleanup: true
|
||||
|
||||
- name: CMake Cache
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
app/.cxx
|
||||
app/src/main/cpp/nettle/nettle
|
||||
key: ${{ runner.os }}-cmake-${{ hashFiles('gradle/libs.versions.toml', 'app/build.gradle.kts', 'app/src/main/cpp/CMakeLists.txt') }}
|
||||
restore-keys: ${{ runner.os }}-cmake-
|
||||
path: app/.cxx
|
||||
key: cmake-default-${{ hashFiles('gradle/libs.versions.toml', 'app/build.gradle.kts', 'app/src/main/cpp/CMakeLists.txt') }}
|
||||
restore-keys: cmake-default-
|
||||
|
||||
- name: Build OSS
|
||||
run: ./gradlew assembleDefaultOssRelease
|
||||
- name: Build
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GithubToken }}
|
||||
run: ./gradlew assembleDefaultRelease
|
||||
|
||||
- name: Upload OSS ARM64
|
||||
uses: actions/upload-artifact@v3
|
||||
- name: Upload ARM64
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: default-oss-arm64-v8a-${{ github.sha }}
|
||||
path: app/build/outputs/apk/defaultOss/release/app-default-oss-arm64-v8a-release.apk
|
||||
name: default-arm64-v8a-${{ github.sha }}
|
||||
path: app/build/outputs/apk/default/release/app-default-arm64-v8a-release.apk
|
||||
|
||||
- name: Upload OSS x86_64
|
||||
uses: actions/upload-artifact@v3
|
||||
- name: Upload x86_64
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: default-oss-x86_64-${{ github.sha }}
|
||||
path: app/build/outputs/apk/defaultOss/release/app-default-oss-x86_64-release.apk
|
||||
name: default-x86_64-${{ github.sha }}
|
||||
path: app/build/outputs/apk/default/release/app-default-x86_64-release.apk
|
||||
|
||||
- name: Upload OSS ARM32
|
||||
uses: actions/upload-artifact@v3
|
||||
- name: Upload ARM32
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: default-oss-armeabi-v7a-${{ github.sha }}
|
||||
path: app/build/outputs/apk/defaultOss/release/app-default-oss-armeabi-v7a-release.apk
|
||||
name: default-armeabi-v7a-${{ github.sha }}
|
||||
path: app/build/outputs/apk/default/release/app-default-armeabi-v7a-release.apk
|
||||
|
||||
- name: Upload OSS x86
|
||||
uses: actions/upload-artifact@v3
|
||||
- name: Upload mapping
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: default-oss-x86-${{ github.sha }}
|
||||
path: app/build/outputs/apk/defaultOss/release/app-default-oss-x86-release.apk
|
||||
name: default-mapping-${{ github.sha }}
|
||||
path: app/build/outputs/mapping/defaultRelease
|
||||
|
||||
- name: Upload OSS mapping
|
||||
uses: actions/upload-artifact@v3
|
||||
- name: Upload native debug symbols
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: default-oss-mapping-${{ github.sha }}
|
||||
path: app/build/outputs/mapping/defaultOssRelease/mapping.txt
|
||||
name: default-native-debug-symbols-${{ github.sha }}
|
||||
path: app/build/outputs/native-debug-symbols/defaultRelease/native-debug-symbols.zip
|
||||
|
||||
- name: Upload OSS native debug symbols
|
||||
uses: actions/upload-artifact@v3
|
||||
marshmallow:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
name: default-oss-native-debug-symbols-${{ github.sha }}
|
||||
path: app/build/outputs/native-debug-symbols/defaultOssRelease/native-debug-symbols.zip
|
||||
distribution: temurin
|
||||
java-version: 21
|
||||
|
||||
- name: Build GMS
|
||||
run: ./gradlew assembleDefaultGmsRelease
|
||||
- name: Install NDK
|
||||
run: echo "y" | ${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager --install "ndk;27.0.11902837"
|
||||
|
||||
- name: Upload GMS ARM64
|
||||
uses: actions/upload-artifact@v3
|
||||
- name: Install Rust Toolchain
|
||||
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
|
||||
with:
|
||||
name: default-gms-arm64-v8a-${{ github.sha }}
|
||||
path: app/build/outputs/apk/defaultGms/release/app-default-gms-arm64-v8a-release.apk
|
||||
workspaces: "./app/src/main/rust"
|
||||
shared-key: ${{ runner.os }}
|
||||
save-if: "false"
|
||||
|
||||
- name: Upload GMS x86_64
|
||||
uses: actions/upload-artifact@v3
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v3
|
||||
with:
|
||||
name: default-gms-x86_64-${{ github.sha }}
|
||||
path: app/build/outputs/apk/defaultGms/release/app-default-gms-x86_64-release.apk
|
||||
cache-encryption-key: ${{ secrets.GradleEncryptionKey }}
|
||||
gradle-home-cache-cleanup: true
|
||||
|
||||
- name: Upload GMS ARM32
|
||||
uses: actions/upload-artifact@v3
|
||||
- name: CMake Cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
name: default-gms-armeabi-v7a-${{ github.sha }}
|
||||
path: app/build/outputs/apk/defaultGms/release/app-default-gms-armeabi-v7a-release.apk
|
||||
path: app/.cxx
|
||||
key: cmake-marshmallow-${{ hashFiles('gradle/libs.versions.toml', 'app/build.gradle.kts', 'app/src/main/cpp/CMakeLists.txt') }}
|
||||
restore-keys: cmake-marshmallow-
|
||||
|
||||
- name: Upload GMS x86
|
||||
uses: actions/upload-artifact@v3
|
||||
- name: Build
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GithubToken }}
|
||||
run: ./gradlew assembleMarshmallowRelease
|
||||
|
||||
- name: Upload ARM64
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: default-gms-x86-${{ github.sha }}
|
||||
path: app/build/outputs/apk/defaultGms/release/app-default-gms-x86-release.apk
|
||||
name: marshmallow-arm64-v8a-${{ github.sha }}
|
||||
path: app/build/outputs/apk/marshmallow/release/app-marshmallow-arm64-v8a-release.apk
|
||||
|
||||
- name: Upload GMS mapping
|
||||
uses: actions/upload-artifact@v3
|
||||
- name: Upload x86_64
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: default-gms-mapping-${{ github.sha }}
|
||||
path: app/build/outputs/mapping/defaultGmsRelease/mapping.txt
|
||||
name: marshmallow-x86_64-${{ github.sha }}
|
||||
path: app/build/outputs/apk/marshmallow/release/app-marshmallow-x86_64-release.apk
|
||||
|
||||
- name: Upload GMS native debug symbols
|
||||
uses: actions/upload-artifact@v3
|
||||
- name: Upload ARM32
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: default-gms-native-debug-symbols-${{ github.sha }}
|
||||
path: app/build/outputs/native-debug-symbols/defaultGmsRelease/native-debug-symbols.zip
|
||||
name: marshmallow-armeabi-v7a-${{ github.sha }}
|
||||
path: app/build/outputs/apk/marshmallow/release/app-marshmallow-armeabi-v7a-release.apk
|
||||
|
||||
- name: Build Marshmallow OSS
|
||||
run: ./gradlew assembleMarshmallowOssRelease
|
||||
|
||||
- name: Upload Marshmallow OSS ARM64
|
||||
uses: actions/upload-artifact@v3
|
||||
- name: Upload mapping
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: marshmallow-oss-arm64-v8a-${{ github.sha }}
|
||||
path: app/build/outputs/apk/marshmallowOss/release/app-marshmallow-oss-arm64-v8a-release.apk
|
||||
name: marshmallow-mapping-${{ github.sha }}
|
||||
path: app/build/outputs/mapping/marshmallowRelease
|
||||
|
||||
- name: Upload Marshmallow OSS x86_64
|
||||
uses: actions/upload-artifact@v3
|
||||
- name: Upload native debug symbols
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: marshmallow-oss-x86_64-${{ github.sha }}
|
||||
path: app/build/outputs/apk/marshmallowOss/release/app-marshmallow-oss-x86_64-release.apk
|
||||
|
||||
- name: Upload Marshmallow OSS ARM32
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: marshmallow-oss-armeabi-v7a-${{ github.sha }}
|
||||
path: app/build/outputs/apk/marshmallowOss/release/app-marshmallow-oss-armeabi-v7a-release.apk
|
||||
|
||||
- name: Upload Marshmallow OSS x86
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: marshmallow-oss-x86-${{ github.sha }}
|
||||
path: app/build/outputs/apk/marshmallowOss/release/app-marshmallow-oss-x86-release.apk
|
||||
|
||||
- name: Upload Marshmallow OSS mapping
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: marshmallow-oss-mapping-${{ github.sha }}
|
||||
path: app/build/outputs/mapping/marshmallowOssRelease/mapping.txt
|
||||
|
||||
- name: Upload Marshmallow OSS native debug symbols
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: marshmallow-oss-native-debug-symbols-${{ github.sha }}
|
||||
path: app/build/outputs/native-debug-symbols/marshmallowOssRelease/native-debug-symbols.zip
|
||||
|
||||
- name: Build Marshmallow GMS
|
||||
run: ./gradlew assembleMarshmallowGmsRelease
|
||||
|
||||
- name: Upload Marshmallow GMS ARM64
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: marshmallow-gms-arm64-v8a-${{ github.sha }}
|
||||
path: app/build/outputs/apk/marshmallowGms/release/app-marshmallow-gms-arm64-v8a-release.apk
|
||||
|
||||
- name: Upload Marshmallow GMS x86_64
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: marshmallow-gms-x86_64-${{ github.sha }}
|
||||
path: app/build/outputs/apk/marshmallowGms/release/app-marshmallow-gms-x86_64-release.apk
|
||||
|
||||
- name: Upload Marshmallow GMS ARM32
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: marshmallow-gms-armeabi-v7a-${{ github.sha }}
|
||||
path: app/build/outputs/apk/marshmallowGms/release/app-marshmallow-gms-armeabi-v7a-release.apk
|
||||
|
||||
- name: Upload Marshmallow GMS x86
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: marshmallow-gms-x86-${{ github.sha }}
|
||||
path: app/build/outputs/apk/marshmallowGms/release/app-marshmallow-gms-x86-release.apk
|
||||
|
||||
- name: Upload Marshmallow GMS mapping
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: marshmallow-gms-mapping-${{ github.sha }}
|
||||
path: app/build/outputs/mapping/marshmallowGmsRelease/mapping.txt
|
||||
|
||||
- name: Upload Marshmallow GMS native debug symbols
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: marshmallow-gms-native-debug-symbols-${{ github.sha }}
|
||||
path: app/build/outputs/native-debug-symbols/marshmallowGmsRelease/native-debug-symbols.zip
|
||||
name: marshmallow-native-debug-symbols-${{ github.sha }}
|
||||
path: app/build/outputs/native-debug-symbols/marshmallowRelease/native-debug-symbols.zip
|
||||
@@ -2,7 +2,7 @@ name: 'Lock Threads'
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
- cron: '0 8 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
@@ -18,5 +18,4 @@ jobs:
|
||||
- uses: dessant/lock-threads@v5
|
||||
with:
|
||||
issue-inactive-days: '1'
|
||||
include-any-issue-labels: 'duplicate, wontfix'
|
||||
process-only: 'issues'
|
||||
@@ -6,23 +6,23 @@ on:
|
||||
- "*"
|
||||
|
||||
jobs:
|
||||
linux:
|
||||
name: Build
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
||||
with:
|
||||
submodules: 'recursive'
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Java 17
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 17
|
||||
java-version: 21
|
||||
|
||||
- name: Install NDK
|
||||
run: echo "y" | ${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager --install "ndk;27.0.11902837"
|
||||
|
||||
- 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
|
||||
@@ -30,86 +30,50 @@ jobs:
|
||||
workspaces: "./app/src/main/rust"
|
||||
|
||||
- name: CMake Cache
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
app/.cxx
|
||||
app/src/main/cpp/nettle/nettle
|
||||
key: ${{ runner.os }}-cmake-${{ hashFiles('gradle/libs.versions.toml', 'app/build.gradle.kts', 'app/src/main/cpp/CMakeLists.txt') }}
|
||||
restore-keys: ${{ runner.os }}-cmake-
|
||||
path: app/.cxx
|
||||
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/gradle-build-action@v2
|
||||
uses: gradle/actions/setup-gradle@v3
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GithubToken }}
|
||||
with:
|
||||
arguments: assembleRelease
|
||||
|
||||
- name: Rename Apks
|
||||
run: |
|
||||
mv app/build/outputs/apk/defaultOss/release/app-default-oss-universal-release.apk EhViewer-${{ github.ref_name }}-default-oss-universal.apk
|
||||
mv app/build/outputs/apk/defaultOss/release/app-default-oss-arm64-v8a-release.apk EhViewer-${{ github.ref_name }}-default-oss-arm64-v8a.apk
|
||||
mv app/build/outputs/apk/defaultOss/release/app-default-oss-x86_64-release.apk EhViewer-${{ github.ref_name }}-default-oss-x86_64.apk
|
||||
mv app/build/outputs/apk/defaultOss/release/app-default-oss-x86-release.apk EhViewer-${{ github.ref_name }}-default-oss-x86.apk
|
||||
mv app/build/outputs/apk/defaultOss/release/app-default-oss-armeabi-v7a-release.apk EhViewer-${{ github.ref_name }}-default-oss-armeabi-v7a.apk
|
||||
mv app/build/outputs/mapping/defaultOssRelease/mapping.txt EhViewer-${{ github.ref_name }}-default-oss-mapping.txt
|
||||
mv app/build/outputs/native-debug-symbols/defaultOssRelease/native-debug-symbols.zip EhViewer-${{ github.ref_name }}-default-oss-native-debug-symbols.zip
|
||||
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-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
|
||||
|
||||
mv app/build/outputs/apk/defaultGms/release/app-default-gms-universal-release.apk EhViewer-${{ github.ref_name }}-default-gms-universal.apk
|
||||
mv app/build/outputs/apk/defaultGms/release/app-default-gms-arm64-v8a-release.apk EhViewer-${{ github.ref_name }}-default-gms-arm64-v8a.apk
|
||||
mv app/build/outputs/apk/defaultGms/release/app-default-gms-x86_64-release.apk EhViewer-${{ github.ref_name }}-default-gms-x86_64.apk
|
||||
mv app/build/outputs/apk/defaultGms/release/app-default-gms-x86-release.apk EhViewer-${{ github.ref_name }}-default-gms-x86.apk
|
||||
mv app/build/outputs/apk/defaultGms/release/app-default-gms-armeabi-v7a-release.apk EhViewer-${{ github.ref_name }}-default-gms-armeabi-v7a.apk
|
||||
mv app/build/outputs/mapping/defaultGmsRelease/mapping.txt EhViewer-${{ github.ref_name }}-default-gms-mapping.txt
|
||||
mv app/build/outputs/native-debug-symbols/defaultGmsRelease/native-debug-symbols.zip EhViewer-${{ github.ref_name }}-default-gms-native-debug-symbols.zip
|
||||
|
||||
mv app/build/outputs/apk/marshmallowOss/release/app-marshmallow-oss-universal-release.apk EhViewer-${{ github.ref_name }}-marshmallow-oss-universal.apk
|
||||
mv app/build/outputs/apk/marshmallowOss/release/app-marshmallow-oss-arm64-v8a-release.apk EhViewer-${{ github.ref_name }}-marshmallow-oss-arm64-v8a.apk
|
||||
mv app/build/outputs/apk/marshmallowOss/release/app-marshmallow-oss-x86_64-release.apk EhViewer-${{ github.ref_name }}-marshmallow-oss-x86_64.apk
|
||||
mv app/build/outputs/apk/marshmallowOss/release/app-marshmallow-oss-x86-release.apk EhViewer-${{ github.ref_name }}-marshmallow-oss-x86.apk
|
||||
mv app/build/outputs/apk/marshmallowOss/release/app-marshmallow-oss-armeabi-v7a-release.apk EhViewer-${{ github.ref_name }}-marshmallow-oss-armeabi-v7a.apk
|
||||
mv app/build/outputs/mapping/marshmallowOssRelease/mapping.txt EhViewer-${{ github.ref_name }}-marshmallow-oss-mapping.txt
|
||||
mv app/build/outputs/native-debug-symbols/marshmallowOssRelease/native-debug-symbols.zip EhViewer-${{ github.ref_name }}-marshmallow-oss-native-debug-symbols.zip
|
||||
|
||||
mv app/build/outputs/apk/marshmallowGms/release/app-marshmallow-gms-universal-release.apk EhViewer-${{ github.ref_name }}-marshmallow-gms-universal.apk
|
||||
mv app/build/outputs/apk/marshmallowGms/release/app-marshmallow-gms-arm64-v8a-release.apk EhViewer-${{ github.ref_name }}-marshmallow-gms-arm64-v8a.apk
|
||||
mv app/build/outputs/apk/marshmallowGms/release/app-marshmallow-gms-x86_64-release.apk EhViewer-${{ github.ref_name }}-marshmallow-gms-x86_64.apk
|
||||
mv app/build/outputs/apk/marshmallowGms/release/app-marshmallow-gms-x86-release.apk EhViewer-${{ github.ref_name }}-marshmallow-gms-x86.apk
|
||||
mv app/build/outputs/apk/marshmallowGms/release/app-marshmallow-gms-armeabi-v7a-release.apk EhViewer-${{ github.ref_name }}-marshmallow-gms-armeabi-v7a.apk
|
||||
mv app/build/outputs/mapping/marshmallowGmsRelease/mapping.txt EhViewer-${{ github.ref_name }}-marshmallow-gms-mapping.txt
|
||||
mv app/build/outputs/native-debug-symbols/marshmallowGmsRelease/native-debug-symbols.zip EhViewer-${{ github.ref_name }}-marshmallow-gms-native-debug-symbols.zip
|
||||
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-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
|
||||
|
||||
- name: Releases
|
||||
uses: softprops/action-gh-release@v1
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
body: Bump Version
|
||||
files: |
|
||||
EhViewer-${{ github.ref_name }}-default-oss-universal.apk
|
||||
EhViewer-${{ github.ref_name }}-default-oss-arm64-v8a.apk
|
||||
EhViewer-${{ github.ref_name }}-default-oss-x86_64.apk
|
||||
EhViewer-${{ github.ref_name }}-default-oss-x86.apk
|
||||
EhViewer-${{ github.ref_name }}-default-oss-armeabi-v7a.apk
|
||||
EhViewer-${{ github.ref_name }}-default-oss-mapping.txt
|
||||
EhViewer-${{ github.ref_name }}-default-oss-native-debug-symbols.zip
|
||||
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-armeabi-v7a.apk
|
||||
EhViewer-${{ github.ref_name }}-default-mapping.txt
|
||||
EhViewer-${{ github.ref_name }}-default-native-debug-symbols.zip
|
||||
|
||||
EhViewer-${{ github.ref_name }}-default-gms-universal.apk
|
||||
EhViewer-${{ github.ref_name }}-default-gms-arm64-v8a.apk
|
||||
EhViewer-${{ github.ref_name }}-default-gms-x86_64.apk
|
||||
EhViewer-${{ github.ref_name }}-default-gms-x86.apk
|
||||
EhViewer-${{ github.ref_name }}-default-gms-armeabi-v7a.apk
|
||||
EhViewer-${{ github.ref_name }}-default-gms-mapping.txt
|
||||
EhViewer-${{ github.ref_name }}-default-gms-native-debug-symbols.zip
|
||||
|
||||
EhViewer-${{ github.ref_name }}-marshmallow-oss-universal.apk
|
||||
EhViewer-${{ github.ref_name }}-marshmallow-oss-arm64-v8a.apk
|
||||
EhViewer-${{ github.ref_name }}-marshmallow-oss-x86_64.apk
|
||||
EhViewer-${{ github.ref_name }}-marshmallow-oss-x86.apk
|
||||
EhViewer-${{ github.ref_name }}-marshmallow-oss-armeabi-v7a.apk
|
||||
EhViewer-${{ github.ref_name }}-marshmallow-oss-mapping.txt
|
||||
EhViewer-${{ github.ref_name }}-marshmallow-oss-native-debug-symbols.zip
|
||||
|
||||
EhViewer-${{ github.ref_name }}-marshmallow-gms-universal.apk
|
||||
EhViewer-${{ github.ref_name }}-marshmallow-gms-arm64-v8a.apk
|
||||
EhViewer-${{ github.ref_name }}-marshmallow-gms-x86_64.apk
|
||||
EhViewer-${{ github.ref_name }}-marshmallow-gms-x86.apk
|
||||
EhViewer-${{ github.ref_name }}-marshmallow-gms-armeabi-v7a.apk
|
||||
EhViewer-${{ github.ref_name }}-marshmallow-gms-mapping.txt
|
||||
EhViewer-${{ github.ref_name }}-marshmallow-gms-native-debug-symbols.zip
|
||||
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-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
-2
@@ -2,8 +2,7 @@
|
||||
/local.properties
|
||||
.DS_Store
|
||||
/build
|
||||
/src
|
||||
/.kotlin
|
||||
*.iml
|
||||
.idea
|
||||
/captures
|
||||
google-services.json
|
||||
+274
-4
@@ -1,6 +1,276 @@
|
||||
# 变更日志
|
||||
|
||||
## 1.10.0
|
||||
## [1.11.6] - 2024-06-24
|
||||
|
||||
### 改进
|
||||
|
||||
* 某些设备上无法加载长图时可尝试减小 设置-高级 中的硬件位图阈值 [#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
|
||||
|
||||
### 改进
|
||||
|
||||
* 优化预加载逻辑和图片加载顺序
|
||||
* 优化画廊详情和评论页面 UI
|
||||
|
||||
### Bug 修复
|
||||
|
||||
* 某些模式下搜索结果不显示画廊语言 [#923](https://github.com/FooIbar/EhViewer/issues/923)
|
||||
* 某些情况下在搜索结果中直接阅读画廊后崩溃 [#927](https://github.com/FooIbar/EhViewer/issues/927)
|
||||
* 更新至最新版本后仍提示有新版本 [#927](https://github.com/FooIbar/EhViewer/issues/927)
|
||||
* 从旧版本升级时包含不兼容数据导致崩溃/无法导出数据 [#940](https://github.com/FooIbar/EhViewer/issues/940) [#974](https://github.com/FooIbar/EhViewer/issues/974)
|
||||
* Android 10 以下的设备打开 设置-隐私 时崩溃 [#953](https://github.com/FooIbar/EhViewer/issues/953)
|
||||
* 下载标签无法拖动
|
||||
* 刷新 和 刷新(原图) 无法在已下载的图片上使用
|
||||
* 临时 IP 封禁错误显示为解析失败
|
||||
* 某些情况下下载时闪退 [#980](https://github.com/FooIbar/EhViewer/issues/980)
|
||||
* 某些设备上下载列表中的缩略图无法显示 [#977](https://github.com/FooIbar/EhViewer/issues/977)
|
||||
|
||||
|
||||
## [1.11.2] - 2024-03-17
|
||||
|
||||
### 重大变更
|
||||
|
||||
* Default 变种最低支持版本更改为 Android 8.0,重新添加了最低支持 Android 6.0 的 Marshmallow 变种
|
||||
|
||||
### 改进
|
||||
|
||||
* 重新添加了删除快速搜索/下载标签时的确认对话框
|
||||
|
||||
### Bug 修复
|
||||
|
||||
* 收藏页面 搜索栏中文标签转英文异常 [#902](https://github.com/FooIbar/EhViewer/issues/902)
|
||||
* 在收藏或者取消收藏之后,收藏按钮会无反应 [#908](https://github.com/FooIbar/EhViewer/issues/908)
|
||||
* 横屏页面下阅读设置UI重合 [#909](https://github.com/FooIbar/EhViewer/issues/909)
|
||||
* Search bar does not work properly in Japanese [#906](https://github.com/FooIbar/EhViewer/issues/906)
|
||||
* 搜索结果中的 Disowned 画廊不显示上传者
|
||||
* 使用 CI 频道更新时,有时会获取到错误的更新链接
|
||||
|
||||
|
||||
## [1.11.1] - 2024-03-12
|
||||
|
||||
### 重大变更
|
||||
|
||||
* 由于 E 站强制要求人机验证,仅支持通过网页登录
|
||||
* 默认支持 Android 6,不再提供 Marshmallow 变种
|
||||
* 集成 Cronet 库,不再提供 GMS 变种
|
||||
|
||||
### 改进
|
||||
|
||||
* 支持在排序下载列表前按下载标签分组
|
||||
|
||||
### Bug 修复
|
||||
|
||||
* 下载画廊时崩溃 [#893](https://github.com/FooIbar/EhViewer/issues/893)
|
||||
|
||||
|
||||
## [1.11.0] - 2024-03-09
|
||||
|
||||
### 重大变更
|
||||
|
||||
* 由于 E 站 Cookie 策略变更,不再支持 Cookie 登录
|
||||
* 登录时自动选择 exhentai (如果可用)
|
||||
* 不再提供默认下载路径
|
||||
* 手动排序更改为以添加时间/上传时间/标题/下载标签/页数对下载列表排序
|
||||
* 由于数据库结构变更,此版本导出的数据将无法被旧版本导入
|
||||
|
||||
### 新功能
|
||||
|
||||
* 下载画廊时生成 ComicInfo.xml 格式元数据
|
||||
* 归档下载时生成 ComicInfo.xml 格式元数据 [#711](https://github.com/FooIbar/EhViewer/issues/711)
|
||||
* 归档下载支持第三方下载管理器 [#79](https://github.com/FooIbar/EhViewer/issues/79)
|
||||
* 保存下载结果为 CBZ 压缩包 [#660](https://github.com/FooIbar/EhViewer/issues/660)
|
||||
* 清除 `igneous` Cookie (将于下次搜索时重新获取)
|
||||
* 随机打开本地收藏/下载中的画廊 [#695](https://github.com/FooIbar/EhViewer/issues/695)
|
||||
* 随机在搜索结果中跳转
|
||||
* 以网格模式查看下载列表
|
||||
* 本地收藏/历史/下载中的标签搜索 [#860](https://github.com/FooIbar/EhViewer/issues/860)
|
||||
|
||||
### 改进
|
||||
|
||||
* 搜索时优先显示已选择的类别卡片
|
||||
* 多处性能优化
|
||||
|
||||
### Bug 修复
|
||||
|
||||
* 显示模式为 Thumbnail 时无法点击上传者卡片 [#653](https://github.com/FooIbar/EhViewer/issues/653)
|
||||
* [Marshmallow] GIF 只显示第一帧
|
||||
* 无法在 `定时请求新闻页面` 关闭时开启 `隐藏 HV 事件通知`
|
||||
* 响应正文含有无效 UTF-8 字符导致解码失败 [#643](https://github.com/FooIbar/EhViewer/issues/643)
|
||||
* 某些情况下应用崩溃 [#710](https://github.com/FooIbar/EhViewer/issues/710)
|
||||
* 画廊名称包含非法路径字符时无法保存图片 [#815](https://github.com/FooIbar/EhViewer/issues/815)
|
||||
* 无法解码超长图片 [#732](https://github.com/FooIbar/EhViewer/issues/732)
|
||||
* E 站变更导致种子解析失败 [#880](https://github.com/FooIbar/EhViewer/issues/880)
|
||||
* E 站不再支持相似扫描导致图片搜索无法使用
|
||||
* 杂项修复
|
||||
|
||||
### 已知问题
|
||||
|
||||
* Android 14 上使用手势导航返回时过渡动画过早结束 [#775](https://github.com/FooIbar/EhViewer/issues/775)
|
||||
|
||||
|
||||
## [1.10.3] - 2024-01-15
|
||||
|
||||
### 新功能
|
||||
|
||||
* [Marshmallow] 支持裁剪边缘、打开压缩包和 GIF
|
||||
|
||||
### 改进
|
||||
|
||||
* 由于部分设备上的非预期行为 [#578](https://github.com/FooIbar/EhViewer/issues/578) [#596](https://github.com/FooIbar/EhViewer/issues/596),默认禁用预见式返回导航动画,可于 设置-高级 中开启
|
||||
* 杂项优化
|
||||
|
||||
### Bug 修复
|
||||
|
||||
* 刷新后页面无响应 [#579](https://github.com/FooIbar/EhViewer/issues/579)
|
||||
* 浏览中保存图片后点击操作被阻挡 [#581](https://github.com/FooIbar/EhViewer/issues/581)
|
||||
* 在画廊详情页面切换至后台后点击评论闪退 [#580](https://github.com/FooIbar/EhViewer/issues/580)
|
||||
* 已下载的画廊中未阅读缓存过的图片解码失败 [#592](https://github.com/FooIbar/EhViewer/issues/592)
|
||||
* 在收藏页多选并下载会使得下载状态出现问题 [#603](https://github.com/FooIbar/EhViewer/issues/603)
|
||||
* 未能在不重启情况下切换 Http 引擎 [#610](https://github.com/FooIbar/EhViewer/issues/610)
|
||||
* 在有进度的画廊阅读界面无法直接回到第一页 [#614](https://github.com/FooIbar/EhViewer/issues/614)
|
||||
* 请求新闻界面失效 [#605](https://github.com/FooIbar/EhViewer/issues/605)
|
||||
* 收藏夹为空解析失败 [#630](https://github.com/FooIbar/EhViewer/issues/630)
|
||||
|
||||
|
||||
## [1.10.2] - 2023-12-31
|
||||
|
||||
### 新功能
|
||||
|
||||
* [阅读器] 支持裁剪图片黑色/白色边缘
|
||||
|
||||
### 改进
|
||||
|
||||
* 切换 HTTP 引擎无需重启
|
||||
* 优化过渡动画
|
||||
* 杂项性能优化
|
||||
|
||||
### Bug 修复
|
||||
|
||||
* 修复搜索建议中出现关键词相同的条目时崩溃 [#533](https://github.com/FooIbar/EhViewer/issues/533) [#555](https://github.com/FooIbar/EhViewer/issues/555)
|
||||
* 修复重命名下载标签时不显示原标签 [#534](https://github.com/FooIbar/EhViewer/issues/534)
|
||||
* 修复调整下载项目顺序时卡顿 [#552](https://github.com/FooIbar/EhViewer/issues/552)
|
||||
* 修复新增或修改评论后黑屏 [#554](https://github.com/FooIbar/EhViewer/issues/554)
|
||||
* 修复剪贴板读取bug [#566](https://github.com/FooIbar/EhViewer/issues/566)
|
||||
* 修复画廊信息对话框无法通过返回按钮/手势关闭 [#413](https://github.com/FooIbar/EhViewer/issues/413)
|
||||
|
||||
|
||||
## [1.10.1] - 2023-12-19
|
||||
|
||||
### 重大变更
|
||||
|
||||
* 由于 Apache HttpClient 5 在 Android 10 以下无法使用,后备 HTTP 引擎恢复为 OkHttp [#514](https://github.com/FooIbar/EhViewer/issues/514)
|
||||
|
||||
### Bug 修复
|
||||
|
||||
* 修复获取 pToken 失败导致无限加载 [#508](https://github.com/FooIbar/EhViewer/issues/508)
|
||||
* 修复阅读页快速滑动滑块导致下载延时过多累计 [#512](https://github.com/FooIbar/EhViewer/issues/512)
|
||||
* 修复 Android 9 以下的设备上无法退出搜索栏 [#513](https://github.com/FooIbar/EhViewer/issues/513)
|
||||
|
||||
|
||||
## [1.10.0] - 2023-12-14 [YANKED]
|
||||
|
||||
### 重大变更
|
||||
|
||||
@@ -24,7 +294,7 @@
|
||||
* 修复 CI 版本不显示变更日志
|
||||
|
||||
|
||||
## 1.9.0
|
||||
## [1.9.0] - 2023-12-01
|
||||
|
||||
### 重大变更
|
||||
|
||||
@@ -38,7 +308,7 @@
|
||||
|
||||
### 改进
|
||||
|
||||
* \[阅读器] 画廊页数过多时隐藏进度条刻度并禁用触感反馈
|
||||
* [阅读器] 画廊页数过多时隐藏进度条刻度并禁用触感反馈
|
||||
* 优化原图下载重试策略
|
||||
* 优化小屏设备上的搜索类别布局
|
||||
* 优化过渡动画
|
||||
@@ -56,7 +326,7 @@
|
||||
* 修复手速过快页面切换过快使加载圈圈不自动消失 [#209](https://github.com/FooIbar/EhViewer/issues/209)
|
||||
* 修复打开并关闭右侧抽屉后,状态栏一片白色 [#101](https://github.com/FooIbar/EhViewer/issues/101)
|
||||
* 修复使用深色主题时,切换界面会闪过亮色界面 [#121](https://github.com/FooIbar/EhViewer/issues/121)
|
||||
* \[Marshmallow] 修复小内存设备上图片解码失败 [#229](https://github.com/FooIbar/EhViewer/issues/229) [#372](https://github.com/FooIbar/EhViewer/issues/372)
|
||||
* [Marshmallow] 修复小内存设备上图片解码失败 [#229](https://github.com/FooIbar/EhViewer/issues/229) [#372](https://github.com/FooIbar/EhViewer/issues/372)
|
||||
* 修复已舍弃画廊上传者解析
|
||||
|
||||
### 已知问题
|
||||
|
||||
@@ -66,16 +66,10 @@ and [Dynamic Color](https://m3.material.io/styles/color/dynamic-color/overview)
|
||||
|
||||
# Download
|
||||
|
||||
| Flavor | Feature |
|
||||
|-------------|-----------------------------------|
|
||||
| Default | Android 9+, full support |
|
||||
| Marshmallow | Android 6.0-8.1, limited support* |
|
||||
| OSS | FOSS dependencies** |
|
||||
| GMS | Proprietary GMS component |
|
||||
|
||||
\* Archives and GIFs are not supported
|
||||
|
||||
** Fall back to Apache5 on devices that don't support [HttpEngine](https://developer.android.com/reference/android/net/http/HttpEngine)
|
||||
| Flavor | Feature |
|
||||
|-------------|----------------------------------|
|
||||
| Default | Android 8.0+, full support |
|
||||
| Marshmallow | Android 6.0-7.1, limited support |
|
||||
|
||||
<a href="https://github.com/FooIbar/EhViewer/releases">
|
||||
<img alt="Get it on GitHub" src="https://github.com/Ehviewer-Overhauled/Art/blob/master/get-it-on-github.svg" width="200px"/>
|
||||
@@ -91,14 +85,13 @@ 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/)
|
||||
- [MDC-Android](https://github.com/material-components/material-components-android)
|
||||
- [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!
|
||||
@@ -108,7 +101,7 @@ Specially thanks [Tachiyomi](https://tachiyomi.org/) for its reader implementati
|
||||
Copyright 2014-2019 Hippo Seven
|
||||
Copyright 2020-2022 NekoInverter
|
||||
Copyright 2022-2023 Tarsin Norbin
|
||||
Copyright 2023 Foolbar
|
||||
Copyright 2023-2024 Foolbar
|
||||
|
||||
EhViewer is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
||||
|
||||
|
||||
@@ -1,8 +1,2 @@
|
||||
/build
|
||||
/src/main/java-gen
|
||||
manifest-merger-release-report.txt
|
||||
/src/main/libs
|
||||
/src/main/obj
|
||||
# Intellij
|
||||
*.iml
|
||||
/.cxx
|
||||
+121
-105
@@ -1,8 +1,6 @@
|
||||
import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties
|
||||
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
|
||||
|
||||
val isRelease: Boolean
|
||||
get() = gradle.startParameter.taskNames.any { it.contains("Release") }
|
||||
@@ -10,26 +8,30 @@ 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.rustAndroidPlugin)
|
||||
alias(libs.plugins.composeCompilerReportGenerator)
|
||||
alias(libs.plugins.baselineprofile)
|
||||
}
|
||||
|
||||
val supportedAbis = arrayOf("arm64-v8a", "x86_64", "armeabi-v7a")
|
||||
|
||||
android {
|
||||
compileSdk = 34
|
||||
buildToolsVersion = "34.0.0"
|
||||
ndkVersion = "26.1.10909125"
|
||||
compileSdk = if (isRelease) 35 else 34
|
||||
buildToolsVersion = "35.0.0"
|
||||
ndkVersion = "27.0.11902837-rc1"
|
||||
androidResources.generateLocaleConfig = true
|
||||
|
||||
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")
|
||||
@@ -50,22 +52,28 @@ 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('.')
|
||||
|
||||
val githubToken = gradleLocalProperties(rootDir, providers)["GITHUB_TOKEN"] as? String
|
||||
?: System.getenv("GITHUB_TOKEN").orEmpty()
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "moe.tarsin.ehviewer"
|
||||
minSdk = 28
|
||||
targetSdk = 34
|
||||
versionCode = 180047
|
||||
versionName = "1.10.0"
|
||||
minSdk = 26
|
||||
targetSdk = 35
|
||||
versionCode = 180057
|
||||
versionName = "1.11.6"
|
||||
// versionNameSuffix = "-SNAPSHOT"
|
||||
resourceConfigurations.addAll(
|
||||
listOf(
|
||||
"zh",
|
||||
@@ -82,39 +90,33 @@ android {
|
||||
"nb-rNO",
|
||||
),
|
||||
)
|
||||
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\"")
|
||||
buildConfigField("String", "GITHUB_TOKEN", "\"$githubToken\"")
|
||||
ndk {
|
||||
if (isRelease) {
|
||||
abiFilters.addAll(supportedAbis)
|
||||
}
|
||||
debugSymbolLevel = "FULL"
|
||||
}
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
arguments += "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
flavorDimensions += listOf("api", "oss")
|
||||
flavorDimensions += "api"
|
||||
|
||||
productFlavors {
|
||||
create("default") {
|
||||
dimension = "api"
|
||||
}
|
||||
create("default")
|
||||
create("marshmallow") {
|
||||
dimension = "api"
|
||||
minSdk = 23
|
||||
applicationIdSuffix = ".m"
|
||||
versionNameSuffix = "-M"
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
targets += "noop"
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
isCoreLibraryDesugaringEnabled = true
|
||||
}
|
||||
}
|
||||
create("oss") {
|
||||
dimension = "oss"
|
||||
}
|
||||
create("gms") {
|
||||
dimension = "oss"
|
||||
versionNameSuffix = "-gms"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,43 +126,19 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
freeCompilerArgs = listOf(
|
||||
// https://kotlinlang.org/docs/compiler-reference.html#progressive
|
||||
"-progressive",
|
||||
"-Xjvm-default=all",
|
||||
"-Xlambdas=indy",
|
||||
|
||||
"-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",
|
||||
)
|
||||
compileOptions {
|
||||
isCoreLibraryDesugaringEnabled = true
|
||||
}
|
||||
|
||||
lint {
|
||||
checkReleaseBuilds = false
|
||||
disable += setOf("MissingTranslation", "MissingQuantity")
|
||||
fatal += setOf("NewApi", "InlineApi")
|
||||
error += setOf("InlinedApi")
|
||||
}
|
||||
|
||||
packaging {
|
||||
resources {
|
||||
excludes += "/META-INF/**"
|
||||
excludes += "/kotlin/**"
|
||||
excludes += "**.txt"
|
||||
excludes += "**.bin"
|
||||
dex {
|
||||
useLegacyPackaging = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,11 +150,19 @@ android {
|
||||
isShrinkResources = true
|
||||
proguardFiles("proguard-rules.pro")
|
||||
signingConfig = signConfig
|
||||
buildConfigField("String", "BUILD_TIME", "\"$buildTime\"")
|
||||
}
|
||||
debug {
|
||||
applicationIdSuffix = ".debug"
|
||||
buildConfigField("String", "BUILD_TIME", "\"\"")
|
||||
lint {
|
||||
abortOnError = false
|
||||
}
|
||||
}
|
||||
create("benchmarkRelease") {
|
||||
initWith(buildTypes.getByName("release"))
|
||||
matchingFallbacks += listOf("release")
|
||||
applicationIdSuffix = ".benchmark"
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
isDebuggable = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,13 +172,29 @@ android {
|
||||
viewBinding = true
|
||||
}
|
||||
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion = libs.versions.compose.compiler.get()
|
||||
}
|
||||
|
||||
namespace = "com.hippo.ehviewer"
|
||||
}
|
||||
|
||||
composeCompiler {
|
||||
enableNonSkippingGroupOptimization = true
|
||||
enableStrongSkippingMode = true
|
||||
}
|
||||
|
||||
androidComponents {
|
||||
onVariants(selector().withBuildType("release")) {
|
||||
it.packaging.resources.excludes.addAll(
|
||||
"/META-INF/**",
|
||||
"/kotlin/**",
|
||||
"**.txt",
|
||||
"**.bin",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
baselineProfile {
|
||||
mergeIntoMain = true
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// https://developer.android.com/jetpack/androidx/releases/activity
|
||||
implementation(libs.androidx.activity.compose)
|
||||
@@ -211,48 +213,54 @@ 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)
|
||||
implementation(libs.androidx.lifecycle.viewmodel.compose)
|
||||
|
||||
// https://developer.android.com/jetpack/androidx/releases/navigation
|
||||
implementation(libs.bundles.androidx.navigation)
|
||||
|
||||
// https://developer.android.com/jetpack/androidx/releases/paging
|
||||
implementation(libs.androidx.paging.compose)
|
||||
|
||||
implementation(libs.androidx.recyclerview)
|
||||
implementation(libs.androidx.viewpager2)
|
||||
|
||||
// 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)
|
||||
implementation(libs.photoview)
|
||||
implementation(libs.material.motion.core)
|
||||
|
||||
implementation(libs.bundles.splitties)
|
||||
|
||||
implementation(libs.okio.jvm)
|
||||
|
||||
implementation(libs.logcat)
|
||||
|
||||
implementation(libs.diff)
|
||||
|
||||
implementation(libs.aboutlibraries.core)
|
||||
implementation(libs.aboutlibraries.compose.m3)
|
||||
implementation(libs.accompanist.drawable.painter)
|
||||
|
||||
implementation(libs.insetter) // Dead Dependency
|
||||
|
||||
implementation(libs.reorderable)
|
||||
// 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))
|
||||
implementation(libs.bundles.coil)
|
||||
|
||||
implementation(libs.bundles.ktor)
|
||||
implementation(libs.telephoto.zoomable)
|
||||
|
||||
implementation(libs.ktor.client.core)
|
||||
|
||||
implementation(libs.bundles.kotlinx.serialization)
|
||||
|
||||
@@ -262,22 +270,45 @@ dependencies {
|
||||
|
||||
coreLibraryDesugaring(libs.desugar)
|
||||
|
||||
"gmsImplementation"(libs.bundles.cronet)
|
||||
}
|
||||
implementation(libs.cronet.embedded)
|
||||
|
||||
configurations.all {
|
||||
resolutionStrategy {
|
||||
exclude("androidx.navigation", "navigation-compose")
|
||||
}
|
||||
implementation(libs.androidx.profileinstaller)
|
||||
"baselineProfile"(project(":benchmark"))
|
||||
|
||||
debugImplementation(libs.compose.ui.tooling)
|
||||
implementation(libs.compose.ui.tooling.preview)
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvmToolchain(17)
|
||||
jvmToolchain(21)
|
||||
compilerOptions {
|
||||
freeCompilerArgs = listOf(
|
||||
// https://kotlinlang.org/docs/compiler-reference.html#progressive
|
||||
"-progressive",
|
||||
"-Xjvm-default=all",
|
||||
"-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.adaptive.ExperimentalMaterial3AdaptiveApi",
|
||||
"-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",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
ksp {
|
||||
arg("room.schemaLocation", "$projectDir/schemas")
|
||||
arg("room.generateKotlin", "true")
|
||||
arg("compose-destinations.codeGenPackageName", "com.hippo.ehviewer.ui")
|
||||
}
|
||||
|
||||
aboutLibraries {
|
||||
@@ -285,14 +316,7 @@ aboutLibraries {
|
||||
duplicationRule = GROUP
|
||||
}
|
||||
|
||||
cargo {
|
||||
module = "src/main/rust"
|
||||
libname = "ehviewer_rust"
|
||||
targets = if (isRelease) listOf("arm", "x86", "arm64", "x86_64") else listOf("arm64", "x86_64")
|
||||
if (isRelease) profile = "release"
|
||||
}
|
||||
|
||||
val ktlintVersion = libs.versions.ktlint.get()
|
||||
val ktlintVersion = libs.ktlint.get().version
|
||||
|
||||
spotless {
|
||||
kotlin {
|
||||
@@ -304,11 +328,3 @@ spotless {
|
||||
ktlint(ktlintVersion)
|
||||
}
|
||||
}
|
||||
|
||||
tasks.configureEach {
|
||||
if (name.startsWith("merge") && name.endsWith("JniLibFolders")) {
|
||||
dependsOn("cargoBuild")
|
||||
// fix mergeDebugJniLibFolders UP-TO-DATE
|
||||
inputs.dir(layout.buildDirectory.dir("rustJniLibs/android"))
|
||||
}
|
||||
}
|
||||
Vendored
+11
-8
@@ -11,18 +11,21 @@
|
||||
public static final ** CREATOR;
|
||||
}
|
||||
|
||||
# https://issuetracker.google.com/347963245
|
||||
-keep class * extends androidx.room.RoomDatabase {
|
||||
<init>();
|
||||
}
|
||||
|
||||
# Ktor logger
|
||||
-dontwarn org.slf4j.impl.StaticLoggerBinder
|
||||
|
||||
# Apache5 Http Client
|
||||
-dontwarn org.ietf.jgss.GSSContext
|
||||
-dontwarn org.ietf.jgss.GSSCredential
|
||||
-dontwarn org.ietf.jgss.GSSException
|
||||
-dontwarn org.ietf.jgss.GSSManager
|
||||
-dontwarn org.ietf.jgss.GSSName
|
||||
-dontwarn org.ietf.jgss.Oid
|
||||
|
||||
-dontwarn org.conscrypt.Conscrypt
|
||||
|
||||
# https://issuetracker.google.com/222232895
|
||||
-dontwarn androidx.window.extensions.**
|
||||
-dontwarn androidx.window.sidecar.Sidecar*
|
||||
|
||||
-keepattributes LineNumberTable
|
||||
|
||||
-allowaccessmodification
|
||||
-repackageclasses
|
||||
@@ -0,0 +1,471 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 17,
|
||||
"identityHash": "d6375317a9298b7feae5925a23272bb9",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "GALLERIES",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`GID` INTEGER NOT NULL, `TOKEN` TEXT, `TITLE` TEXT, `TITLE_JPN` TEXT, `THUMB` TEXT, `CATEGORY` INTEGER NOT NULL, `POSTED` TEXT, `UPLOADER` TEXT, `RATING` REAL NOT NULL, `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": false
|
||||
},
|
||||
{
|
||||
"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": "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, `POSITION` INTEGER NOT NULL, 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
|
||||
},
|
||||
{
|
||||
"fieldPath": "position",
|
||||
"columnName": "POSITION",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"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": "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": []
|
||||
}
|
||||
],
|
||||
"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, 'd6375317a9298b7feae5925a23272bb9')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,471 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 18,
|
||||
"identityHash": "bc90acf4ffb4e7cce9a16cd5a0f662f3",
|
||||
"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_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": "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, `POSITION` INTEGER NOT NULL, 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
|
||||
},
|
||||
{
|
||||
"fieldPath": "position",
|
||||
"columnName": "POSITION",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"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": "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": []
|
||||
}
|
||||
],
|
||||
"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, 'bc90acf4ffb4e7cce9a16cd5a0f662f3')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,465 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 19,
|
||||
"identityHash": "edd2f8e628072889f7702b50eb2c44a1",
|
||||
"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_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": "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": "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": []
|
||||
}
|
||||
],
|
||||
"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, 'edd2f8e628072889f7702b50eb2c44a1')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,472 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 20,
|
||||
"identityHash": "a8dd0226b286cc57a8c99ba7ad416730",
|
||||
"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, `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": "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": "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": []
|
||||
}
|
||||
],
|
||||
"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, 'a8dd0226b286cc57a8c99ba7ad416730')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,513 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 21,
|
||||
"identityHash": "6a9926f6722f8fc6f9c4426b31260e33",
|
||||
"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, `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": "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": "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_GalleryFts_BEFORE_UPDATE BEFORE UPDATE ON `GALLERIES` BEGIN DELETE FROM `GalleryFts` WHERE `docid`=OLD.`rowid`; END",
|
||||
"CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_GalleryFts_BEFORE_DELETE BEFORE DELETE ON `GALLERIES` BEGIN DELETE FROM `GalleryFts` WHERE `docid`=OLD.`rowid`; END",
|
||||
"CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_GalleryFts_AFTER_UPDATE AFTER UPDATE ON `GALLERIES` BEGIN INSERT INTO `GalleryFts`(`docid`, `TITLE`, `TITLE_JPN`) VALUES (NEW.`rowid`, NEW.`TITLE`, NEW.`TITLE_JPN`); END",
|
||||
"CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_GalleryFts_AFTER_INSERT AFTER INSERT ON `GALLERIES` BEGIN INSERT INTO `GalleryFts`(`docid`, `TITLE`, `TITLE_JPN`) VALUES (NEW.`rowid`, NEW.`TITLE`, NEW.`TITLE_JPN`); END"
|
||||
],
|
||||
"tableName": "GalleryFts",
|
||||
"createSql": "CREATE VIRTUAL TABLE IF NOT EXISTS `${TABLE_NAME}` USING FTS4(`TITLE` TEXT NOT NULL, `TITLE_JPN` TEXT NOT NULL, content=`GALLERIES`)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "title",
|
||||
"columnName": "TITLE",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "titleJpn",
|
||||
"columnName": "TITLE_JPN",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"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, '6a9926f6722f8fc6f9c4426b31260e33')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,525 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 22,
|
||||
"identityHash": "f61ae3beef3739c0de7db945d2c9a4a4",
|
||||
"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": "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, 'f61ae3beef3739c0de7db945d2c9a4a4')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,176 +0,0 @@
|
||||
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
|
||||
import io.ktor.http.HttpHeaders
|
||||
import io.ktor.http.HttpProtocolVersion
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.http.content.OutgoingContent
|
||||
import io.ktor.http.headers
|
||||
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.useInstance
|
||||
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.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.channels.consumeEach
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import org.chromium.net.CronetException
|
||||
import org.chromium.net.UploadDataProvider
|
||||
import org.chromium.net.UploadDataSink
|
||||
import org.chromium.net.UrlRequest
|
||||
import org.chromium.net.UrlResponseInfo
|
||||
|
||||
object CronetEngine : HttpClientEngineBase("Cronet") {
|
||||
override val config = HttpClientEngineConfig()
|
||||
override val dispatcher = cronetDispatcher
|
||||
|
||||
@InternalAPI
|
||||
override suspend fun execute(data: HttpRequestData): HttpResponseData {
|
||||
val callContext = callContext()
|
||||
|
||||
return executeHttpRequest(callContext, data)
|
||||
}
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
private suspend fun executeHttpRequest(
|
||||
callContext: CoroutineContext,
|
||||
data: HttpRequestData,
|
||||
): HttpResponseData = suspendCancellableCoroutine { continuation ->
|
||||
val requestTime = GMTDate()
|
||||
|
||||
val callback = object : UrlRequest.Callback() {
|
||||
lateinit var chunkChan: Channel<ByteBuffer>
|
||||
override fun onRedirectReceived(request: UrlRequest, info: UrlResponseInfo, newLocationUrl: String) {
|
||||
continuation.resume(
|
||||
info.toHttpResponseData(
|
||||
requestTime = requestTime,
|
||||
callContext = callContext,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
override fun onResponseStarted(request: UrlRequest, info: UrlResponseInfo) {
|
||||
val channel = GlobalScope.writer(callContext) {
|
||||
pool.useInstance {
|
||||
chunkChan = Channel(1)
|
||||
request.read(it)
|
||||
chunkChan.consumeEach { buffer ->
|
||||
buffer.flip()
|
||||
channel.writeFully(buffer)
|
||||
buffer.clear()
|
||||
request.read(buffer)
|
||||
}
|
||||
}
|
||||
}.channel
|
||||
continuation.resume(
|
||||
info.toHttpResponseData(
|
||||
requestTime = requestTime,
|
||||
callContext = callContext,
|
||||
responseBody = channel,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
override fun onReadCompleted(request: UrlRequest, info: UrlResponseInfo, byteBuffer: ByteBuffer) {
|
||||
chunkChan.trySend(byteBuffer).getOrThrow()
|
||||
}
|
||||
|
||||
override fun onSucceeded(request: UrlRequest, info: UrlResponseInfo) {
|
||||
chunkChan.close()
|
||||
}
|
||||
|
||||
override fun onFailed(request: UrlRequest, info: UrlResponseInfo?, error: CronetException) {
|
||||
if (::chunkChan.isInitialized) {
|
||||
chunkChan.close(error)
|
||||
} else {
|
||||
continuation.resumeWithException(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val request = cronetHttpClient.newUrlRequestBuilder(data.url.toString(), callback, cronetHttpClientExecutor).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(callContext)?.let { setUploadDataProvider(it, cronetHttpClientExecutor) }
|
||||
}.build()
|
||||
request.start()
|
||||
callContext[Job]!!.invokeOnCompletion { request.cancel() }
|
||||
continuation.invokeOnCancellation { request.cancel() }
|
||||
}
|
||||
}
|
||||
|
||||
private fun UrlResponseInfo.toHttpResponseData(
|
||||
requestTime: GMTDate,
|
||||
callContext: CoroutineContext,
|
||||
responseBody: ByteReadChannel = ByteReadChannel.Empty,
|
||||
): HttpResponseData {
|
||||
return HttpResponseData(
|
||||
statusCode = HttpStatusCode.fromValue(httpStatusCode),
|
||||
requestTime = requestTime,
|
||||
headers = headers {
|
||||
allHeaders.forEach { (key, value) ->
|
||||
appendAll(key, value)
|
||||
}
|
||||
},
|
||||
version = when (negotiatedProtocol) {
|
||||
"h2" -> HttpProtocolVersion.HTTP_2_0
|
||||
"h3" -> HttpProtocolVersion.QUIC
|
||||
"quic/1+spdy/3" -> HttpProtocolVersion.SPDY_3
|
||||
else -> HttpProtocolVersion.HTTP_1_1
|
||||
},
|
||||
body = responseBody,
|
||||
callContext = callContext,
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
private fun OutgoingContent.toUploadDataProvider(callContext: CoroutineContext): UploadDataProvider? = when (this) {
|
||||
is OutgoingContent.NoContent -> null
|
||||
is OutgoingContent.ByteArrayContent -> object : UploadDataProvider() {
|
||||
val buffer = ByteBuffer.wrap(bytes()).slice()
|
||||
override fun getLength() = buffer.limit().toLong()
|
||||
override fun read(uploadDataSink: UploadDataSink, byteBuffer: ByteBuffer) {
|
||||
byteBuffer.put(buffer)
|
||||
uploadDataSink.onReadSucceeded(false)
|
||||
}
|
||||
override fun rewind(uploadDataSink: UploadDataSink) {
|
||||
buffer.position(0)
|
||||
uploadDataSink.onRewindSucceeded()
|
||||
}
|
||||
}
|
||||
is OutgoingContent.WriteChannelContent -> object : UploadDataProvider() {
|
||||
fun launch() = GlobalScope.writer(callContext) { writeTo(channel) }.channel
|
||||
var chan = launch()
|
||||
override fun getLength() = contentLength!!
|
||||
override fun read(uploadDataSink: UploadDataSink, byteBuffer: ByteBuffer) {
|
||||
GlobalScope.launch(callContext) {
|
||||
chan.readAvailable(byteBuffer)
|
||||
uploadDataSink.onReadSucceeded(false)
|
||||
}
|
||||
}
|
||||
override fun rewind(uploadDataSink: UploadDataSink) {
|
||||
chan = launch()
|
||||
uploadDataSink.onRewindSucceeded()
|
||||
}
|
||||
}
|
||||
else -> error("UnsupportedContentType $this")
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package com.hippo.ehviewer.util
|
||||
|
||||
import com.google.android.gms.net.CronetProviderInstaller
|
||||
import org.chromium.net.CronetProvider
|
||||
import splitties.init.appCtx
|
||||
|
||||
val isCronetAvailable: Boolean
|
||||
get() = CronetProvider.getAllProviders(appCtx).any { it.isEnabled }.also {
|
||||
if (!it) {
|
||||
CronetProviderInstaller.installProvider(appCtx)
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
@@ -37,11 +36,6 @@
|
||||
tools:targetApi="tiramisu">
|
||||
<profileable android:shell="true" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.ConfigureActivity"
|
||||
android:exported="false"
|
||||
android:launchMode="singleTask"
|
||||
android:windowSoftInputMode="adjustResize" />
|
||||
<activity
|
||||
android:name=".ui.MainActivity"
|
||||
android:banner="@mipmap/ic_leanback_banner"
|
||||
@@ -106,23 +100,24 @@
|
||||
<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/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" />
|
||||
|
||||
<!-- untested -->
|
||||
<data android:mimeType="application/vnd.comicbook+zip" />
|
||||
<data android:mimeType="application/vnd.comicbook-rar" />
|
||||
<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" />
|
||||
@@ -139,7 +134,6 @@
|
||||
android:launchMode="singleTask"
|
||||
android:taskAffinity=""
|
||||
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
|
||||
<activity android:name=".ui.WebViewActivity" />
|
||||
|
||||
<service
|
||||
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
From d1db03ebd9989d10a5a905b792b712c6487a0057 Mon Sep 17 00:00:00 2001
|
||||
From: FooIbar <118464521+FooIbar@users.noreply.github.com>
|
||||
Date: Sat, 24 Feb 2024 12:03:15 +0000
|
||||
Subject: [PATCH] Don't build other binaries
|
||||
|
||||
---
|
||||
CMakeLists.txt | 8 ++++----
|
||||
1 file changed, 4 insertions(+), 4 deletions(-)
|
||||
|
||||
diff --git a/CMakeLists.txt b/CMakeLists.txt
|
||||
index a346dd02..52b5d0d8 100644
|
||||
--- a/CMakeLists.txt
|
||||
+++ b/CMakeLists.txt
|
||||
@@ -1567,7 +1567,7 @@ endif()
|
||||
# xzdec and lzmadec
|
||||
#############################################################################
|
||||
|
||||
-if(HAVE_DECODERS AND (NOT MSVC OR MSVC_VERSION GREATER_EQUAL 1900))
|
||||
+if(0)
|
||||
foreach(XZDEC xzdec lzmadec)
|
||||
add_executable("${XZDEC}"
|
||||
src/common/sysdefs.h
|
||||
@@ -1624,7 +1624,7 @@ endif()
|
||||
# lzmainfo
|
||||
#############################################################################
|
||||
|
||||
-if(HAVE_DECODERS AND (NOT MSVC OR MSVC_VERSION GREATER_EQUAL 1900))
|
||||
+if(0)
|
||||
add_executable(lzmainfo
|
||||
src/common/sysdefs.h
|
||||
src/common/tuklib_common.h
|
||||
@@ -1680,7 +1680,7 @@ endif()
|
||||
# xz
|
||||
#############################################################################
|
||||
|
||||
-if(NOT MSVC OR MSVC_VERSION GREATER_EQUAL 1900)
|
||||
+if(0)
|
||||
add_executable(xz
|
||||
src/common/mythread.h
|
||||
src/common/sysdefs.h
|
||||
@@ -1914,7 +1914,7 @@ endif()
|
||||
# Scripts
|
||||
#############################################################################
|
||||
|
||||
-if(UNIX)
|
||||
+if(0)
|
||||
# NOTE: This isn't as sophisticated as in the Autotools build which
|
||||
# uses posix-shell.m4 but hopefully this doesn't need to be either.
|
||||
# CMake likely won't be used on as many (old) obscure systems as the
|
||||
--
|
||||
2.34.1
|
||||
|
||||
@@ -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 ASM)
|
||||
project(ehviewer C)
|
||||
include(ExternalProject)
|
||||
include(FetchContent)
|
||||
|
||||
if (NOT CMAKE_BUILD_TYPE STREQUAL "Debug")
|
||||
@@ -16,33 +17,49 @@ if (NOT CMAKE_BUILD_TYPE STREQUAL "Debug")
|
||||
-mllvm -polly-invariant-load-hoisting")
|
||||
endif (NOT CMAKE_BUILD_TYPE STREQUAL "Debug")
|
||||
|
||||
# Change the default path of fetch content to avoid downloading the same dependencies per architecture
|
||||
get_filename_component(deps "../_deps" REALPATH BASE_DIR "${CMAKE_BINARY_DIR}")
|
||||
set(FETCHCONTENT_BASE_DIR ${deps})
|
||||
option(BUILD_TESTING OFF)
|
||||
|
||||
set(BUILD_TESTING 0)
|
||||
|
||||
FetchContent_Declare(liblzma
|
||||
GIT_REPOSITORY https://github.com/FooIbar/xz.git
|
||||
GIT_TAG v5.4.5
|
||||
)
|
||||
set(XZ_PATCH ${CMAKE_CURRENT_LIST_DIR}/0001-Don-t-build-other-binaries.patch)
|
||||
FetchContent_Declare(
|
||||
liblzma
|
||||
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(nettle
|
||||
URL https://ftp.gnu.org/gnu/nettle/nettle-3.9.1.tar.gz
|
||||
URL_MD5 29fcd2dec6bf5b48e5e3ffb3cbc4779e
|
||||
SOURCE_DIR ${CMAKE_CURRENT_LIST_DIR}/nettle/nettle
|
||||
)
|
||||
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
|
||||
PREFIX nettle
|
||||
URL https://ftp.gnu.org/gnu/nettle/nettle-3.10.tar.gz
|
||||
URL_MD5 c61453139d5fb44e9cdcc5b684b26e55
|
||||
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)
|
||||
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(nettle)
|
||||
include_directories(.)
|
||||
set(HAVE_LIBNETTLE 1)
|
||||
set(HAVE_NETTLE_AES_H 1)
|
||||
set(HAVE_NETTLE_HMAC_H 1)
|
||||
@@ -58,6 +75,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,18 +83,29 @@ 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")
|
||||
|
||||
FetchContent_Declare(libarchive
|
||||
GIT_REPOSITORY https://github.com/FooIbar/libarchive.git
|
||||
GIT_TAG v3.7.2
|
||||
)
|
||||
set(LIBARCHIVE_PATCH ${CMAKE_CURRENT_LIST_DIR}/0001-Insert-link-libs.patch)
|
||||
FetchContent_Declare(
|
||||
libarchive
|
||||
GIT_REPOSITORY https://github.com/libarchive/libarchive.git
|
||||
GIT_TAG v3.7.4
|
||||
PATCH_COMMAND git apply --check -R ${LIBARCHIVE_PATCH} || git apply ${LIBARCHIVE_PATCH}
|
||||
)
|
||||
|
||||
FetchContent_MakeAvailable(libarchive)
|
||||
include_directories(${libarchive_SOURCE_DIR}/libarchive)
|
||||
|
||||
# Build and link our app's native lib
|
||||
add_library(${PROJECT_NAME} SHARED archive.c gifutils.c natsort/strnatcmp.c)
|
||||
target_link_libraries(${PROJECT_NAME} archive_static log)
|
||||
FetchContent_Declare(
|
||||
Corrosion
|
||||
GIT_REPOSITORY https://github.com/corrosion-rs/corrosion.git
|
||||
GIT_TAG v0.5
|
||||
)
|
||||
|
||||
add_custom_target("noop")
|
||||
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")
|
||||
|
||||
# Build and link our app's native lib
|
||||
add_library(${PROJECT_NAME} SHARED archive.c border.c gifutils.c hash.c natsort/strnatcmp.c)
|
||||
target_link_libraries(${PROJECT_NAME} ehviewer_rust archive_static log jnigraphics -Wl,--exclude-libs,ALL)
|
||||
+130
-93
@@ -34,7 +34,6 @@
|
||||
|
||||
#include "natsort/strnatcmp.h"
|
||||
#include "ehviewer.h"
|
||||
#include "spinlock.h"
|
||||
|
||||
typedef struct {
|
||||
int using;
|
||||
@@ -49,18 +48,18 @@ typedef struct {
|
||||
size_t size;
|
||||
} entry;
|
||||
|
||||
_Atomic mcs_lock ctx_lock;
|
||||
static archive_ctx **ctx_pool;
|
||||
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
static archive_ctx **ctx_pool = NULL;
|
||||
#define CTX_POOL_SIZE 20
|
||||
|
||||
static void *mempool = NULL;
|
||||
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 MEMPOOL_ENTITY_SIZE(x) PAGE_ALIGN(entries[x].size)
|
||||
|
||||
#define PROT_RW (PROT_WRITE | PROT_READ)
|
||||
#define MAP_ANON_POOL (MAP_ANONYMOUS | MAP_NORESERVE | MAP_PRIVATE | MAP_UNINITIALIZED)
|
||||
@@ -86,6 +85,8 @@ const char supportExt[10][6] = {
|
||||
};
|
||||
|
||||
static inline int filename_is_playable_file(const char *name) {
|
||||
if (!name)
|
||||
return false;
|
||||
const char *dotptr = strrchr(name, '.');
|
||||
if (!dotptr++)
|
||||
return false;
|
||||
@@ -96,26 +97,39 @@ static inline int filename_is_playable_file(const char *name) {
|
||||
return false;
|
||||
}
|
||||
|
||||
static inline const char *archive_entry_pathname2(struct archive_entry *entry) {
|
||||
const char *name = archive_entry_pathname_utf8(entry);
|
||||
return name ? name : archive_entry_pathname(entry);
|
||||
}
|
||||
|
||||
static inline bool archive_entry_is_file(struct archive_entry *entry) {
|
||||
return archive_entry_filetype(entry) == AE_IFREG;
|
||||
}
|
||||
|
||||
static inline bool archive_entry_is_playable(struct archive_entry *entry) {
|
||||
return archive_entry_is_file(entry) &&
|
||||
filename_is_playable_file(archive_entry_pathname2(entry));
|
||||
}
|
||||
|
||||
static inline int compare_entries(const void *a, const void *b) {
|
||||
const char *fa = ((entry *) a)->filename;
|
||||
const char *fb = ((entry *) b)->filename;
|
||||
return strnatcmp(fa, fb);
|
||||
}
|
||||
|
||||
static long archive_map_entries_index(archive_ctx *ctx) {
|
||||
static long archive_map_entries_index(archive_ctx *ctx, bool sort) {
|
||||
long count = 0;
|
||||
while (archive_read_next_header(ctx->arc, &ctx->entry) == ARCHIVE_OK) {
|
||||
const char *name = archive_entry_pathname(ctx->entry);
|
||||
if (filename_is_playable_file(name)) {
|
||||
const char *name = archive_entry_pathname2(ctx->entry);
|
||||
if (archive_entry_is_file(ctx->entry) && filename_is_playable_file(name)) {
|
||||
entries[count].filename = strdup(name);
|
||||
entries[count].index = count;
|
||||
entries[count].size = archive_entry_size(ctx->entry);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
if (!count)
|
||||
LOGE("%s", archive_error_string(ctx->arc));
|
||||
qsort(entries, entryCount, sizeof(entry), compare_entries);
|
||||
if (sort)
|
||||
qsort(entries, entryCount, sizeof(entry), compare_entries);
|
||||
return count;
|
||||
}
|
||||
|
||||
@@ -135,39 +149,27 @@ static bool archive_prealloc_mempool() {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// Mincore does not support anon mapping yet
|
||||
// Now that we use MADV_FREE, how can we detect whether pages is reclaimed?
|
||||
static bool is_mempool_pages_present_and_lock(int index) {
|
||||
void *addr = MEMPOOL_ADDR_BY_SORTED_IDX(index);
|
||||
size_t size = MEMPOOL_ENTITY_SIZE(index);
|
||||
mlock(addr, size);
|
||||
#if 0
|
||||
unsigned char vec[size / PAGE_SIZE];
|
||||
unsigned char r = 1;
|
||||
mincore(addr, size, vec);
|
||||
for (int i = 0; i < size / PAGE_SIZE; ++i) r &= vec[i];
|
||||
return r;
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
|
||||
#define MEMPOOL_IN_CORE_AND_LOCK(x) is_mempool_pages_present_and_lock(x)
|
||||
#define ADDR_IN_FILE_MAPPING(addr) (addr >= archiveAddr && addr < archiveAddr + archiveSize)
|
||||
|
||||
static void mempool_release_pages(void *addr, size_t size) {
|
||||
size = PAGE_ALIGN(size);
|
||||
munlock(addr, size);
|
||||
int ret = madvise(addr, size, MADV_FREE);
|
||||
if (ret == -EINVAL) madvise(addr, size, MADV_DONTNEED);
|
||||
madvise_log_if_error(addr, size, MADV_DONTNEED);
|
||||
}
|
||||
|
||||
static bool kernel_can_prefault = true;
|
||||
|
||||
static void mempool_prefault_pages(void *addr, size_t size) {
|
||||
size = PAGE_ALIGN(size);
|
||||
if (kernel_can_prefault) {
|
||||
if (madvise(addr, size, MADV_POPULATE_WRITE)) kernel_can_prefault = false;
|
||||
}
|
||||
}
|
||||
|
||||
static long archive_list_all_entries(archive_ctx *ctx) {
|
||||
long count = 0;
|
||||
while (archive_read_next_header(ctx->arc, &ctx->entry) == ARCHIVE_OK)
|
||||
if (filename_is_playable_file(archive_entry_pathname(ctx->entry)))
|
||||
if (archive_entry_is_playable(ctx->entry))
|
||||
count++;
|
||||
if (!count)
|
||||
LOGE("%s", archive_error_string(ctx->arc));
|
||||
return count;
|
||||
}
|
||||
|
||||
@@ -212,7 +214,7 @@ static int archive_alloc_ctx(archive_ctx **ctxptr) {
|
||||
|
||||
static int archive_skip_to_index(archive_ctx *ctx, int index) {
|
||||
while (archive_read_next_header(ctx->arc, &ctx->entry) == ARCHIVE_OK) {
|
||||
if (!filename_is_playable_file(archive_entry_pathname(ctx->entry)))
|
||||
if (!archive_entry_is_playable(ctx->entry))
|
||||
continue;
|
||||
if (ctx->next_index++ == index) {
|
||||
return ctx->next_index - 1;
|
||||
@@ -224,8 +226,7 @@ static int archive_skip_to_index(archive_ctx *ctx, int index) {
|
||||
static int archive_get_ctx(archive_ctx **ctxptr, int idx) {
|
||||
int ret;
|
||||
archive_ctx *ctx = NULL;
|
||||
mcs_lock_t local_lock;
|
||||
lock_mcs(&ctx_lock, &local_lock);
|
||||
pthread_mutex_lock(&mutex);
|
||||
for (int i = 0; i < CTX_POOL_SIZE; i++) {
|
||||
if (!ctx_pool[i])
|
||||
continue;
|
||||
@@ -240,7 +241,7 @@ static int archive_get_ctx(archive_ctx **ctxptr, int idx) {
|
||||
}
|
||||
if (ctx)
|
||||
ctx->using = 1;
|
||||
unlock_mcs(&ctx_lock, &local_lock);
|
||||
pthread_mutex_unlock(&mutex);
|
||||
|
||||
if (!ctx) {
|
||||
archive_ctx *victimCtx = NULL;
|
||||
@@ -249,7 +250,7 @@ static int archive_get_ctx(archive_ctx **ctxptr, int idx) {
|
||||
ret = archive_alloc_ctx(&ctx);
|
||||
if (ret)
|
||||
return ret;
|
||||
lock_mcs(&ctx_lock, &local_lock);
|
||||
pthread_mutex_lock(&mutex);
|
||||
for (int i = 0; i < CTX_POOL_SIZE; i++) {
|
||||
if (!ctx_pool[i]) {
|
||||
ctx_pool[i] = ctx;
|
||||
@@ -264,7 +265,7 @@ static int archive_get_ctx(archive_ctx **ctxptr, int idx) {
|
||||
}
|
||||
}
|
||||
if (replace) ctx_pool[victimIdx] = ctx;
|
||||
unlock_mcs(&ctx_lock, &local_lock);
|
||||
pthread_mutex_unlock(&mutex);
|
||||
if (replace) archive_release_ctx(victimCtx);
|
||||
}
|
||||
ret = archive_skip_to_index(ctx, idx);
|
||||
@@ -279,19 +280,17 @@ static int archive_get_ctx(archive_ctx **ctxptr, int idx) {
|
||||
}
|
||||
|
||||
JNIEXPORT jint JNICALL
|
||||
Java_com_hippo_ehviewer_jni_ArchiveKt_openArchive(JNIEnv *env, jclass thiz, jint fd,
|
||||
jlong size) {
|
||||
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;
|
||||
int mmap_flags = MAP_PRIVATE;
|
||||
archiveAddr = mmap64(0, size, PROT_READ, mmap_flags, fd, 0);
|
||||
archiveAddr = mmap(0, size, PROT_READ, MAP_PRIVATE, fd, 0);
|
||||
if (archiveAddr == MAP_FAILED) {
|
||||
LOGE("%s%s", "mmap64 failed with error ", strerror(errno));
|
||||
LOGE("%s%s", "mmap failed with error ", strerror(errno));
|
||||
return 0;
|
||||
}
|
||||
archiveSize = size;
|
||||
madvise_log_if_error(archiveAddr, archiveSize, MADV_WILLNEED);
|
||||
ctx_pool = calloc(CTX_POOL_SIZE, sizeof(archive_ctx **));
|
||||
if (!ctx_pool) {
|
||||
LOGE("Allocate archive ctx pool failed:ENOMEM");
|
||||
@@ -305,6 +304,11 @@ Java_com_hippo_ehviewer_jni_ArchiveKt_openArchive(JNIEnv *env, jclass thiz, jint
|
||||
} else {
|
||||
entryCount = archive_list_all_entries(ctx);
|
||||
LOGI("%s%zu%s", "Found ", entryCount, " image entries in archive");
|
||||
if (!entryCount) {
|
||||
LOGE("%s%s", "Archive open failed:", archive_error_string(ctx->arc));
|
||||
archive_release_ctx(ctx);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// We must read through the file|vm then we can know whether it is encrypted
|
||||
int encryptRet = archive_read_has_encrypted_entries(ctx->arc);
|
||||
@@ -340,7 +344,7 @@ Java_com_hippo_ehviewer_jni_ArchiveKt_openArchive(JNIEnv *env, jclass thiz, jint
|
||||
LOGE("%s%s", "Archive open failed:", archive_error_string(ctx->arc));
|
||||
} else {
|
||||
entries = calloc(entryCount, sizeof(entry));
|
||||
r = archive_map_entries_index(ctx);
|
||||
r = archive_map_entries_index(ctx, sort_entries);
|
||||
if (!archive_prealloc_mempool()) {
|
||||
r = 0;
|
||||
for (int i = 0; i < entryCount; ++i) {
|
||||
@@ -354,44 +358,52 @@ Java_com_hippo_ehviewer_jni_ArchiveKt_openArchive(JNIEnv *env, jclass thiz, jint
|
||||
return r;
|
||||
}
|
||||
|
||||
#pragma clang diagnostic push
|
||||
#pragma ide diagnostic ignored "ConstantConditionsOC"
|
||||
#pragma ide diagnostic ignored "UnreachableCode"
|
||||
|
||||
JNIEXPORT jobject JNICALL
|
||||
Java_com_hippo_ehviewer_jni_ArchiveKt_extractToByteBuffer(JNIEnv *env, jclass thiz,
|
||||
jint index) {
|
||||
Java_com_hippo_ehviewer_jni_ArchiveKt_extractToByteBuffer(JNIEnv *env, jclass thiz, jint index) {
|
||||
EH_UNUSED(env);
|
||||
EH_UNUSED(thiz);
|
||||
void *addr = MEMPOOL_ADDR_BY_SORTED_IDX(index);
|
||||
size_t size = entries[index].size;
|
||||
if (!MEMPOOL_IN_CORE_AND_LOCK(index)) {
|
||||
index = entries[index].index;
|
||||
archive_ctx *ctx = NULL;
|
||||
int ret;
|
||||
ret = archive_get_ctx(&ctx, index);
|
||||
if (ret) return 0;
|
||||
ret = archive_read_data(ctx->arc, addr, size);
|
||||
index = entries[index].index;
|
||||
archive_ctx *ctx = NULL;
|
||||
int ret = archive_get_ctx(&ctx, index);
|
||||
if (ret) return 0;
|
||||
void *buffer = NULL;
|
||||
size_t buffer_size = 0;
|
||||
la_int64_t output_ofs = 0;
|
||||
archive_read_data_block(ctx->arc, (const void **) &buffer, &buffer_size, &output_ofs);
|
||||
bool zero_copy = ADDR_IN_FILE_MAPPING(buffer) && !output_ofs && buffer_size == size;
|
||||
if (zero_copy) {
|
||||
ctx->using = 0;
|
||||
if (ret == size)
|
||||
return (*env)->NewDirectByteBuffer(env, addr, size);
|
||||
if (ret != size)
|
||||
LOGE("%s", "No enough data read, WTF?");
|
||||
if (ret < 0)
|
||||
LOGE("%s%s", "Archive read failed:", archive_error_string(ctx->arc));
|
||||
mempool_release_pages(addr, size);
|
||||
return (*env)->NewDirectByteBuffer(env, buffer, buffer_size);
|
||||
} else {
|
||||
return (*env)->NewDirectByteBuffer(env, addr, size);
|
||||
size_t bytes = 0;
|
||||
mempool_prefault_pages(addr, size);
|
||||
do {
|
||||
memcpy(addr + output_ofs, buffer, buffer_size);
|
||||
bytes += buffer_size;
|
||||
ret = archive_read_data_block(ctx->arc, (const void **) &buffer, &buffer_size, &output_ofs);
|
||||
} while (bytes != size && !ret);
|
||||
ctx->using = 0;
|
||||
if (bytes == size) {
|
||||
return (*env)->NewDirectByteBuffer(env, addr, size);
|
||||
} else {
|
||||
if (ret < 0) {
|
||||
LOGE("%s%s", "Archive read failed:", archive_error_string(ctx->arc));
|
||||
} else {
|
||||
LOGE("%s", "No enough data read, WTF?");
|
||||
}
|
||||
}
|
||||
mempool_release_pages(addr, size);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
#pragma clang diagnostic pop
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_hippo_ehviewer_jni_ArchiveKt_closeArchive(JNIEnv *env, jclass thiz) {
|
||||
EH_UNUSED(env);
|
||||
EH_UNUSED(thiz);
|
||||
if (!ctx_pool) return;
|
||||
for (int i = 0; i < CTX_POOL_SIZE; i++)
|
||||
archive_release_ctx(ctx_pool[i]);
|
||||
free(passwd);
|
||||
@@ -415,8 +427,7 @@ Java_com_hippo_ehviewer_jni_ArchiveKt_needPassword(JNIEnv *env, jclass thiz) {
|
||||
}
|
||||
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_com_hippo_ehviewer_jni_ArchiveKt_providePassword(JNIEnv *env, jclass thiz,
|
||||
jstring str) {
|
||||
Java_com_hippo_ehviewer_jni_ArchiveKt_providePassword(JNIEnv *env, jclass thiz, jstring str) {
|
||||
EH_UNUSED(thiz);
|
||||
struct archive_entry *entry;
|
||||
archive_ctx *ctx;
|
||||
@@ -427,11 +438,11 @@ Java_com_hippo_ehviewer_jni_ArchiveKt_providePassword(JNIEnv *env, jclass thiz,
|
||||
LOGE("Allocate passwd buffer failed");
|
||||
return false;
|
||||
}
|
||||
strcpy(passwd, (*env)->GetStringUTFChars(env, str, NULL));
|
||||
(*env)->GetStringUTFRegion(env, str, 0, len, passwd);
|
||||
archive_alloc_ctx(&ctx);
|
||||
void *tmpBuf = alloca(4096);
|
||||
while (archive_read_next_header(ctx->arc, &entry) == ARCHIVE_OK) {
|
||||
if (!filename_is_playable_file(archive_entry_pathname(entry)))
|
||||
if (!archive_entry_is_playable(entry))
|
||||
continue;
|
||||
if (!archive_entry_is_encrypted(entry))
|
||||
continue;
|
||||
@@ -446,24 +457,15 @@ Java_com_hippo_ehviewer_jni_ArchiveKt_providePassword(JNIEnv *env, jclass thiz,
|
||||
}
|
||||
|
||||
JNIEXPORT jstring JNICALL
|
||||
Java_com_hippo_ehviewer_jni_ArchiveKt_getFilename(JNIEnv *env, jclass thiz,
|
||||
jint index) {
|
||||
Java_com_hippo_ehviewer_jni_ArchiveKt_getExtension(JNIEnv *env, jclass thiz, jint index) {
|
||||
EH_UNUSED(env);
|
||||
EH_UNUSED(thiz);
|
||||
index = entries[index].index;
|
||||
archive_ctx *ctx = NULL;
|
||||
int ret;
|
||||
ret = archive_get_ctx(&ctx, index);
|
||||
if (ret)
|
||||
return NULL;
|
||||
jstring str = (*env)->NewStringUTF(env, archive_entry_pathname(ctx->entry));
|
||||
ctx->using = 0;
|
||||
return str;
|
||||
const char *ext = strrchr(entries[index].filename, '.') + 1;
|
||||
return (*env)->NewStringUTF(env, ext);
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_hippo_ehviewer_jni_ArchiveKt_extractToFd(JNIEnv *env, jclass thiz,
|
||||
jint index, jint fd) {
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_com_hippo_ehviewer_jni_ArchiveKt_extractToFd(JNIEnv *env, jclass thiz, jint index, jint fd) {
|
||||
EH_UNUSED(env);
|
||||
EH_UNUSED(thiz);
|
||||
index = entries[index].index;
|
||||
@@ -471,16 +473,51 @@ Java_com_hippo_ehviewer_jni_ArchiveKt_extractToFd(JNIEnv *env, jclass thiz,
|
||||
int ret;
|
||||
ret = archive_get_ctx(&ctx, index);
|
||||
if (!ret) {
|
||||
archive_read_data_into_fd(ctx->arc, fd);
|
||||
ret = archive_read_data_into_fd(ctx->arc, fd);
|
||||
ctx->using = 0;
|
||||
}
|
||||
return ret == ARCHIVE_OK;
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_hippo_ehviewer_jni_ArchiveKt_releaseByteBuffer(JNIEnv *env, jclass thiz,
|
||||
jobject buffer) {
|
||||
Java_com_hippo_ehviewer_jni_ArchiveKt_releaseByteBuffer(JNIEnv *env, jclass thiz, jobject buffer) {
|
||||
EH_UNUSED(thiz);
|
||||
void *addr = (*env)->GetDirectBufferAddress(env, buffer);
|
||||
size_t size = (*env)->GetDirectBufferCapacity(env, buffer);
|
||||
mempool_release_pages(addr, size);
|
||||
if (!ADDR_IN_FILE_MAPPING(addr)) mempool_release_pages(addr, size);
|
||||
}
|
||||
|
||||
JNIEXPORT jboolean 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();
|
||||
struct stat st;
|
||||
char buff[8192];
|
||||
jint fdBatch[size];
|
||||
(*env)->GetIntArrayRegion(env, fd_batch, 0, size, fdBatch);
|
||||
archive_write_set_format_zip(arc);
|
||||
archive_write_zip_set_compression_store(arc);
|
||||
archive_write_open_fd(arc, arc_fd);
|
||||
struct archive_entry *entry = archive_entry_new();
|
||||
for (int i = 0; i < size; i++) {
|
||||
int fd = fdBatch[i];
|
||||
jobject name = (*env)->GetObjectArrayElement(env, names, i);
|
||||
const char *cname = (*env)->GetStringUTFChars(env, name, false);
|
||||
archive_entry_set_pathname(entry, cname);
|
||||
(*env)->ReleaseStringUTFChars(env, name, cname);
|
||||
fstat(fd, &st);
|
||||
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);
|
||||
len = read(fd, buff, sizeof(buff));
|
||||
}
|
||||
archive_write_finish_entry(arc);
|
||||
archive_entry_clear(entry);
|
||||
}
|
||||
archive_write_close(arc);
|
||||
archive_write_free(arc);
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
#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,6 @@
|
||||
TOOLCHAIN=$ANDROID_HOME/ndk/$1/toolchains/llvm/prebuilt/$(uname -s | tr '[:upper:]' '[:lower:]')-x86_64/bin
|
||||
TARGET=$2
|
||||
./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);
|
||||
}
|
||||
@@ -84,30 +84,6 @@ compare_right(nat_char const *a, nat_char const *b)
|
||||
} else if (!*a && !*b)
|
||||
return bias;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
static int
|
||||
compare_left(nat_char const *a, nat_char const *b)
|
||||
{
|
||||
/* Compare two left-aligned numbers: the first to have a
|
||||
different value wins. */
|
||||
for (;; a++, b++) {
|
||||
if (!nat_isdigit(*a) && !nat_isdigit(*b))
|
||||
return 0;
|
||||
if (!nat_isdigit(*a))
|
||||
return -1;
|
||||
if (!nat_isdigit(*b))
|
||||
return +1;
|
||||
if (*a < *b)
|
||||
return -1;
|
||||
if (*a > *b)
|
||||
return +1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -116,30 +92,23 @@ strnatcmp0(nat_char const *a, nat_char const *b, int fold_case)
|
||||
{
|
||||
int ai, bi;
|
||||
nat_char ca, cb;
|
||||
int fractional, result;
|
||||
int result;
|
||||
|
||||
ai = bi = 0;
|
||||
while (1) {
|
||||
ca = a[ai]; cb = b[bi];
|
||||
|
||||
/* skip over leading spaces or zeros */
|
||||
while (nat_isspace(ca))
|
||||
while (nat_isspace(ca) || ca == '0')
|
||||
ca = a[++ai];
|
||||
|
||||
while (nat_isspace(cb))
|
||||
while (nat_isspace(cb) || cb == '0')
|
||||
cb = b[++bi];
|
||||
|
||||
/* process run of digits */
|
||||
if (nat_isdigit(ca) && nat_isdigit(cb)) {
|
||||
fractional = (ca == '0' || cb == '0');
|
||||
|
||||
if (fractional) {
|
||||
if ((result = compare_left(a+ai, b+bi)) != 0)
|
||||
return result;
|
||||
} else {
|
||||
if ((result = compare_right(a+ai, b+bi)) != 0)
|
||||
return result;
|
||||
}
|
||||
if ((result = compare_right(a+ai, b+bi)) != 0)
|
||||
return result;
|
||||
}
|
||||
|
||||
if (!ca && !cb) {
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
/nettle
|
||||
@@ -1,238 +0,0 @@
|
||||
cmake_minimum_required(VERSION 3.4.1)
|
||||
project(nettle C)
|
||||
|
||||
set(LIBNETTLE_DEFINITIONS
|
||||
-DHAVE_CONFIG_H)
|
||||
|
||||
set(LIBNETTLE_SOURCES
|
||||
nettle/aes-decrypt-internal.c
|
||||
nettle/aes-decrypt.c
|
||||
nettle/aes-decrypt-table.c
|
||||
nettle/aes128-decrypt.c
|
||||
nettle/aes192-decrypt.c
|
||||
nettle/aes256-decrypt.c
|
||||
nettle/aes-encrypt-internal.c
|
||||
nettle/aes-encrypt.c
|
||||
nettle/aes-encrypt-table.c
|
||||
nettle/aes128-encrypt.c
|
||||
nettle/aes192-encrypt.c
|
||||
nettle/aes256-encrypt.c
|
||||
nettle/aes-invert-internal.c
|
||||
nettle/aes-set-key-internal.c
|
||||
nettle/aes-set-encrypt-key.c
|
||||
nettle/aes-set-decrypt-key.c
|
||||
nettle/aes128-set-encrypt-key.c
|
||||
nettle/aes128-set-decrypt-key.c
|
||||
nettle/aes128-meta.c
|
||||
nettle/aes192-set-encrypt-key.c
|
||||
nettle/aes192-set-decrypt-key.c
|
||||
nettle/aes192-meta.c
|
||||
nettle/aes256-set-encrypt-key.c
|
||||
nettle/aes256-set-decrypt-key.c
|
||||
nettle/aes256-meta.c
|
||||
nettle/nist-keywrap.c
|
||||
nettle/arcfour.c
|
||||
nettle/arctwo.c
|
||||
nettle/arctwo-meta.c
|
||||
nettle/blowfish.c
|
||||
nettle/blowfish-bcrypt.c
|
||||
nettle/base16-encode.c
|
||||
nettle/base16-decode.c
|
||||
nettle/base16-meta.c
|
||||
nettle/base64-encode.c
|
||||
nettle/base64-decode.c
|
||||
nettle/base64-meta.c
|
||||
nettle/base64url-encode.c
|
||||
nettle/base64url-decode.c
|
||||
nettle/base64url-meta.c
|
||||
nettle/buffer.c
|
||||
nettle/buffer-init.c
|
||||
nettle/camellia-crypt-internal.c
|
||||
nettle/camellia-table.c
|
||||
nettle/camellia-absorb.c
|
||||
nettle/camellia-invert-key.c
|
||||
nettle/camellia128-set-encrypt-key.c
|
||||
nettle/camellia128-crypt.c
|
||||
nettle/camellia128-set-decrypt-key.c
|
||||
nettle/camellia128-meta.c
|
||||
nettle/camellia192-meta.c
|
||||
nettle/camellia256-set-encrypt-key.c
|
||||
nettle/camellia256-crypt.c
|
||||
nettle/camellia256-set-decrypt-key.c
|
||||
nettle/camellia256-meta.c
|
||||
nettle/cast128.c
|
||||
nettle/cast128-meta.c
|
||||
nettle/cbc.c
|
||||
nettle/cbc-aes128-encrypt.c
|
||||
nettle/cbc-aes192-encrypt.c
|
||||
nettle/cbc-aes256-encrypt.c
|
||||
nettle/ccm.c
|
||||
nettle/ccm-aes128.c
|
||||
nettle/ccm-aes192.c
|
||||
nettle/ccm-aes256.c
|
||||
nettle/cfb.c
|
||||
nettle/siv-cmac.c
|
||||
nettle/siv-cmac-aes128.c
|
||||
nettle/siv-cmac-aes256.c
|
||||
nettle/cnd-memcpy.c
|
||||
nettle/chacha-crypt.c
|
||||
nettle/chacha-core-internal.c
|
||||
nettle/chacha-poly1305.c
|
||||
nettle/chacha-poly1305-meta.c
|
||||
nettle/chacha-set-key.c
|
||||
nettle/chacha-set-nonce.c
|
||||
nettle/ctr.c
|
||||
nettle/ctr16.c
|
||||
nettle/des.c
|
||||
nettle/des3.c
|
||||
nettle/eax.c
|
||||
nettle/eax-aes128.c
|
||||
nettle/eax-aes128-meta.c
|
||||
nettle/ghash-set-key.c
|
||||
nettle/ghash-update.c
|
||||
nettle/gcm.c
|
||||
nettle/gcm-aes.c
|
||||
nettle/gcm-aes128.c
|
||||
nettle/gcm-aes128-meta.c
|
||||
nettle/gcm-aes192.c
|
||||
nettle/gcm-aes192-meta.c
|
||||
nettle/gcm-aes256.c
|
||||
nettle/gcm-aes256-meta.c
|
||||
nettle/gcm-camellia128.c
|
||||
nettle/gcm-camellia128-meta.c
|
||||
nettle/gcm-camellia256.c
|
||||
nettle/gcm-camellia256-meta.c
|
||||
nettle/cmac.c
|
||||
nettle/cmac64.c
|
||||
nettle/cmac-aes128.c
|
||||
nettle/cmac-aes256.c
|
||||
nettle/cmac-des3.c
|
||||
nettle/cmac-aes128-meta.c
|
||||
nettle/cmac-aes256-meta.c
|
||||
nettle/cmac-des3-meta.c
|
||||
nettle/gost28147.c
|
||||
nettle/gosthash94.c
|
||||
nettle/gosthash94-meta.c
|
||||
nettle/hmac.c
|
||||
nettle/hmac-gosthash94.c
|
||||
nettle/hmac-md5.c
|
||||
nettle/hmac-ripemd160.c
|
||||
nettle/hmac-sha1.c
|
||||
nettle/hmac-sha224.c
|
||||
nettle/hmac-sha256.c
|
||||
nettle/hmac-sha384.c
|
||||
nettle/hmac-sha512.c
|
||||
nettle/hmac-streebog.c
|
||||
nettle/hmac-sm3.c
|
||||
nettle/hmac-md5-meta.c
|
||||
nettle/hmac-ripemd160-meta.c
|
||||
nettle/hmac-sha1-meta.c
|
||||
nettle/hmac-sha224-meta.c
|
||||
nettle/hmac-sha256-meta.c
|
||||
nettle/hmac-sha384-meta.c
|
||||
nettle/hmac-sha512-meta.c
|
||||
nettle/hmac-streebog-meta.c
|
||||
nettle/hmac-sm3-meta.c
|
||||
nettle/knuth-lfib.c
|
||||
nettle/hkdf.c
|
||||
nettle/md2.c
|
||||
nettle/md2-meta.c
|
||||
nettle/md4.c
|
||||
nettle/md4-meta.c
|
||||
nettle/md5.c
|
||||
nettle/md5-compress.c
|
||||
nettle/md5-compat.c
|
||||
nettle/md5-meta.c
|
||||
nettle/memeql-sec.c
|
||||
nettle/memxor.c
|
||||
nettle/memxor3.c
|
||||
nettle/nettle-lookup-hash.c
|
||||
nettle/nettle-meta-aeads.c
|
||||
nettle/nettle-meta-armors.c
|
||||
nettle/nettle-meta-ciphers.c
|
||||
nettle/nettle-meta-hashes.c
|
||||
nettle/nettle-meta-macs.c
|
||||
nettle/pbkdf2.c
|
||||
nettle/pbkdf2-hmac-gosthash94.c
|
||||
nettle/pbkdf2-hmac-sha1.c
|
||||
nettle/pbkdf2-hmac-sha256.c
|
||||
nettle/pbkdf2-hmac-sha384.c
|
||||
nettle/pbkdf2-hmac-sha512.c
|
||||
nettle/poly1305-aes.c
|
||||
nettle/poly1305-internal.c
|
||||
nettle/realloc.c
|
||||
nettle/ripemd160.c
|
||||
nettle/ripemd160-compress.c
|
||||
nettle/ripemd160-meta.c
|
||||
nettle/salsa20-core-internal.c
|
||||
nettle/salsa20-crypt-internal.c
|
||||
nettle/salsa20-crypt.c
|
||||
nettle/salsa20r12-crypt.c
|
||||
nettle/salsa20-set-key.c
|
||||
nettle/salsa20-set-nonce.c
|
||||
nettle/salsa20-128-set-key.c
|
||||
nettle/salsa20-256-set-key.c
|
||||
nettle/sha1.c
|
||||
nettle/sha1-compress.c
|
||||
nettle/sha1-meta.c
|
||||
nettle/sha256.c
|
||||
nettle/sha256-compress-n.c
|
||||
nettle/sha224-meta.c
|
||||
nettle/sha256-meta.c
|
||||
nettle/sha512.c
|
||||
nettle/sha512-compress.c
|
||||
nettle/sha384-meta.c
|
||||
nettle/sha512-meta.c
|
||||
nettle/sha512-224-meta.c
|
||||
nettle/sha512-256-meta.c
|
||||
nettle/sha3.c
|
||||
nettle/sha3-permute.c
|
||||
nettle/sha3-224.c
|
||||
nettle/sha3-224-meta.c
|
||||
nettle/sha3-256.c
|
||||
nettle/sha3-256-meta.c
|
||||
nettle/sha3-384.c
|
||||
nettle/sha3-384-meta.c
|
||||
nettle/sha3-512.c
|
||||
nettle/sha3-512-meta.c
|
||||
nettle/shake256.c
|
||||
nettle/sm3.c
|
||||
nettle/sm3-meta.c
|
||||
nettle/serpent-set-key.c
|
||||
nettle/serpent-encrypt.c
|
||||
nettle/serpent-decrypt.c
|
||||
nettle/serpent-meta.c
|
||||
nettle/streebog.c
|
||||
nettle/streebog-meta.c
|
||||
nettle/twofish.c
|
||||
nettle/twofish-meta.c
|
||||
nettle/umac-nh.c
|
||||
nettle/umac-nh-n.c
|
||||
nettle/umac-l2.c
|
||||
nettle/umac-l3.c
|
||||
nettle/umac-poly64.c
|
||||
nettle/umac-poly128.c
|
||||
nettle/umac-set-key.c
|
||||
nettle/umac32.c
|
||||
nettle/umac64.c
|
||||
nettle/umac96.c
|
||||
nettle/umac128.c
|
||||
nettle/version.c
|
||||
nettle/write-be32.c
|
||||
nettle/write-le32.c
|
||||
nettle/write-le64.c
|
||||
nettle/yarrow256.c
|
||||
nettle/yarrow_key_event.c
|
||||
nettle/xts.c
|
||||
nettle/xts-aes128.c
|
||||
nettle/xts-aes256.c
|
||||
)
|
||||
|
||||
set(LIBNETTLE_INCLUDES
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/nettle
|
||||
)
|
||||
|
||||
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 */
|
||||
@@ -1,44 +0,0 @@
|
||||
#ifndef EHVIEWER_SPINLOCK_H
|
||||
#define EHVIEWER_SPINLOCK_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdatomic.h>
|
||||
|
||||
#define cmpxchg(P, O, N) __sync_val_compare_and_swap((P), (O), (N))
|
||||
|
||||
#define barrier() __asm__ __volatile__("":::"memory")
|
||||
|
||||
#if defined(__aarch64__) || defined(__arm__)
|
||||
#define cpu_relax() __asm__ volatile ("wfe");
|
||||
#else
|
||||
#define cpu_relax() __asm__ volatile ("pause" ::: "memory");
|
||||
#endif
|
||||
|
||||
typedef struct mcs_lock_t mcs_lock_t;
|
||||
struct mcs_lock_t {
|
||||
mcs_lock_t *next;
|
||||
int spin;
|
||||
};
|
||||
typedef struct mcs_lock_t *mcs_lock;
|
||||
|
||||
static inline void lock_mcs(_Atomic mcs_lock *m, mcs_lock_t *me) {
|
||||
mcs_lock_t *tail;
|
||||
me->next = NULL;
|
||||
me->spin = 0;
|
||||
tail = atomic_exchange(m, me);
|
||||
if (!tail) return;
|
||||
tail->next = me;
|
||||
barrier();
|
||||
while (!me->spin) cpu_relax();
|
||||
return;
|
||||
}
|
||||
|
||||
static inline void unlock_mcs(mcs_lock *m, mcs_lock_t *me) {
|
||||
if (!me->next) {
|
||||
if (cmpxchg(m, me, NULL) == me) return;
|
||||
while (!me->next) cpu_relax();
|
||||
}
|
||||
me->next->spin = 1;
|
||||
}
|
||||
|
||||
#endif //EHVIEWER_SPINLOCK_H
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
+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,474 @@
|
||||
/*
|
||||
* 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 = scrollState.layoutInfo.visibleItemsInfo
|
||||
val indexOfFirstKnownLineItem = visibleItemsInfo.indexOfFirst { it.line() != unknownLine }
|
||||
if (indexOfFirstKnownLineItem == -1) {
|
||||
return 0.0
|
||||
}
|
||||
val reallyVisibleItemsInfo = // Non-exiting visible items
|
||||
visibleItemsInfo.subList(indexOfFirstKnownLineItem, visibleItemsInfo.size)
|
||||
|
||||
// Compute the size of the last line
|
||||
val lastLine = reallyVisibleItemsInfo.last().line()
|
||||
val lastLineSize = reallyVisibleItemsInfo
|
||||
.asReversed()
|
||||
.asSequence()
|
||||
.takeWhile { it.line() == lastLine }
|
||||
.maxOf { it.mainAxisSize() }
|
||||
|
||||
val first = reallyVisibleItemsInfo.first()
|
||||
val last = reallyVisibleItemsInfo.last()
|
||||
val lineCount = last.line() - 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,989 +0,0 @@
|
||||
@file:Suppress("ktlint:standard:property-naming", "ktlint:standard:function-naming")
|
||||
|
||||
package androidx.compose.material3
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.graphics.PixelFormat
|
||||
import android.os.Build
|
||||
import android.view.Gravity
|
||||
import android.view.View
|
||||
import android.view.ViewTreeObserver
|
||||
import android.view.WindowManager
|
||||
import androidx.activity.OnBackPressedDispatcher
|
||||
import androidx.activity.OnBackPressedDispatcherOwner
|
||||
import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
|
||||
import androidx.compose.animation.core.SpringSpec
|
||||
import androidx.compose.animation.core.TweenSpec
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.gestures.AnchoredDraggableState
|
||||
import androidx.compose.foundation.gestures.DraggableAnchors
|
||||
import androidx.compose.foundation.gestures.Orientation
|
||||
import androidx.compose.foundation.gestures.anchoredDraggable
|
||||
import androidx.compose.foundation.gestures.animateTo
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.foundation.gestures.snapTo
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionContext
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCompositionContext
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.runtime.saveable.Saver
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.graphics.isSpecified
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.platform.AbstractComposeView
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.compose.ui.platform.ViewRootForInspector
|
||||
import androidx.compose.ui.semantics.clearAndSetSemantics
|
||||
import androidx.compose.ui.semantics.popup
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.unit.Density
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import androidx.compose.ui.unit.Velocity
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.lerp
|
||||
import androidx.compose.ui.window.SecureFlagPolicy
|
||||
import androidx.lifecycle.findViewTreeLifecycleOwner
|
||||
import androidx.lifecycle.findViewTreeViewModelStoreOwner
|
||||
import androidx.lifecycle.setViewTreeLifecycleOwner
|
||||
import androidx.lifecycle.setViewTreeViewModelStoreOwner
|
||||
import androidx.savedstate.findViewTreeSavedStateRegistryOwner
|
||||
import androidx.savedstate.setViewTreeSavedStateRegistryOwner
|
||||
import com.hippo.ehviewer.ui.tools.animateFloatMergeOneWayPredictiveBackAsState
|
||||
import com.hippo.ehviewer.util.isAtLeastT
|
||||
import java.util.UUID
|
||||
import kotlin.math.absoluteValue
|
||||
import kotlin.math.max
|
||||
import kotlin.math.roundToInt
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.drop
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Stable
|
||||
@ExperimentalMaterial3Api
|
||||
internal object AnchoredDraggableDefaults2 {
|
||||
/**
|
||||
* The default animation used by [AnchoredDraggableState].
|
||||
*/
|
||||
@get:ExperimentalMaterial3Api
|
||||
@Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
|
||||
@ExperimentalMaterial3Api
|
||||
val AnimationSpec = SpringSpec<Float>()
|
||||
}
|
||||
|
||||
/**
|
||||
* State of a sheet composable, such as [ModalBottomSheetFix]
|
||||
*
|
||||
* Contains states relating to its swipe position as well as animations between state values.
|
||||
*
|
||||
* @param skipPartiallyExpanded Whether the partially expanded state, if the sheet is large
|
||||
* enough, should be skipped. If true, the sheet will always expand to the [Expanded] state and move
|
||||
* to the [Hidden] state if available when hiding the sheet, either programmatically or by user
|
||||
* interaction.
|
||||
* @param initialValue The initial value of the state.
|
||||
* @param confirmValueChange Optional callback invoked to confirm or veto a pending state change.
|
||||
* @param skipHiddenState Whether the hidden state should be skipped. If true, the sheet will always
|
||||
* expand to the [Expanded] state and move to the [PartiallyExpanded] if available, either
|
||||
* programmatically or by user interaction.
|
||||
*/
|
||||
@Stable
|
||||
@ExperimentalMaterial3Api
|
||||
class SheetState2
|
||||
@Deprecated(
|
||||
message = "This constructor is deprecated. " +
|
||||
"Please use the constructor that provides a [Density]",
|
||||
replaceWith = ReplaceWith(
|
||||
"SheetState(" +
|
||||
"skipPartiallyExpanded, LocalDensity.current, initialValue, " +
|
||||
"confirmValueChange, skipHiddenState)",
|
||||
),
|
||||
)
|
||||
constructor(
|
||||
internal val skipPartiallyExpanded: Boolean,
|
||||
initialValue: SheetValue = SheetValue.Hidden,
|
||||
val confirmValueChange: (SheetValue) -> Boolean = { true },
|
||||
internal val skipHiddenState: Boolean = false,
|
||||
) {
|
||||
|
||||
/**
|
||||
* State of a sheet composable, such as [ModalBottomSheetFix]
|
||||
*
|
||||
* Contains states relating to its swipe position as well as animations between state values.
|
||||
*
|
||||
* @param skipPartiallyExpanded Whether the partially expanded state, if the sheet is large
|
||||
* enough, should be skipped. If true, the sheet will always expand to the [Expanded] state and move
|
||||
* to the [Hidden] state if available when hiding the sheet, either programmatically or by user
|
||||
* interaction.
|
||||
* @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.
|
||||
* @param skipHiddenState Whether the hidden state should be skipped. If true, the sheet will always
|
||||
* expand to the [Expanded] state and move to the [PartiallyExpanded] if available, either
|
||||
* programmatically or by user interaction.
|
||||
*/
|
||||
@ExperimentalMaterial3Api
|
||||
@Suppress("Deprecation")
|
||||
constructor(
|
||||
skipPartiallyExpanded: Boolean,
|
||||
density: Density,
|
||||
initialValue: SheetValue = SheetValue.Hidden,
|
||||
confirmValueChange: (SheetValue) -> Boolean = { true },
|
||||
skipHiddenState: Boolean = false,
|
||||
) : this(skipPartiallyExpanded, initialValue, confirmValueChange, skipHiddenState) {
|
||||
this.density = density
|
||||
}
|
||||
init {
|
||||
if (skipPartiallyExpanded) {
|
||||
require(initialValue != SheetValue.PartiallyExpanded) {
|
||||
"The initial value must not be set to PartiallyExpanded if skipPartiallyExpanded " +
|
||||
"is set to true."
|
||||
}
|
||||
}
|
||||
if (skipHiddenState) {
|
||||
require(initialValue != SheetValue.Hidden) {
|
||||
"The initial value must not be set to Hidden if skipHiddenState is set to true."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The current value of the state.
|
||||
*
|
||||
* If no swipe or animation is in progress, this corresponds to the state the bottom sheet is
|
||||
* currently in. If a swipe or an animation is in progress, this corresponds the state the sheet
|
||||
* was in before the swipe or animation started.
|
||||
*/
|
||||
|
||||
val currentValue: SheetValue get() = anchoredDraggableState.currentValue
|
||||
|
||||
/**
|
||||
* The target value of the bottom sheet state.
|
||||
*
|
||||
* If a swipe is in progress, this is the value that the sheet would animate to if the
|
||||
* swipe finishes. If an animation is running, this is the target value of that animation.
|
||||
* Finally, if no swipe or animation is in progress, this is the same as the [currentValue].
|
||||
*/
|
||||
val targetValue: SheetValue get() = anchoredDraggableState.targetValue
|
||||
|
||||
/**
|
||||
* Whether the modal bottom sheet is visible.
|
||||
*/
|
||||
val isVisible: Boolean
|
||||
get() = anchoredDraggableState.currentValue != SheetValue.Hidden
|
||||
|
||||
/**
|
||||
* Require the current offset (in pixels) of the bottom sheet.
|
||||
*
|
||||
* The offset will be initialized during the first measurement phase of the provided sheet
|
||||
* content.
|
||||
*
|
||||
* These are the phases:
|
||||
* Composition { -> Effects } -> Layout { Measurement -> Placement } -> Drawing
|
||||
*
|
||||
* During the first composition, an [IllegalStateException] is thrown. In subsequent
|
||||
* compositions, the offset will be derived from the anchors of the previous pass. Always prefer
|
||||
* accessing the offset from a LaunchedEffect as it will be scheduled to be executed the next
|
||||
* frame, after layout.
|
||||
*
|
||||
* @throws IllegalStateException If the offset has not been initialized yet
|
||||
*/
|
||||
fun requireOffset(): Float = anchoredDraggableState.requireOffset()
|
||||
|
||||
/**
|
||||
* Whether the sheet has an expanded state defined.
|
||||
*/
|
||||
|
||||
val hasExpandedState: Boolean
|
||||
get() = anchoredDraggableState.anchors.hasAnchorFor(SheetValue.Expanded)
|
||||
|
||||
/**
|
||||
* Whether the modal bottom sheet has a partially expanded state defined.
|
||||
*/
|
||||
val hasPartiallyExpandedState: Boolean
|
||||
get() = anchoredDraggableState.anchors.hasAnchorFor(SheetValue.PartiallyExpanded)
|
||||
|
||||
/**
|
||||
* Fully expand the bottom sheet with animation and suspend until it is fully expanded or
|
||||
* animation has been cancelled.
|
||||
* *
|
||||
* @throws [CancellationException] if the animation is interrupted
|
||||
*/
|
||||
suspend fun expand() {
|
||||
anchoredDraggableState.animateTo(SheetValue.Expanded)
|
||||
}
|
||||
|
||||
/**
|
||||
* Animate the bottom sheet and suspend until it is partially expanded or animation has been
|
||||
* cancelled.
|
||||
* @throws [CancellationException] if the animation is interrupted
|
||||
* @throws [IllegalStateException] if [skipPartiallyExpanded] is set to true
|
||||
*/
|
||||
suspend fun partialExpand() {
|
||||
check(!skipPartiallyExpanded) {
|
||||
"Attempted to animate to partial expanded when skipPartiallyExpanded was enabled. Set" +
|
||||
" skipPartiallyExpanded to false to use this function."
|
||||
}
|
||||
animateTo(SheetValue.PartiallyExpanded)
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand the bottom sheet with animation and suspend until it is [PartiallyExpanded] if defined
|
||||
* else [Expanded].
|
||||
* @throws [CancellationException] if the animation is interrupted
|
||||
*/
|
||||
suspend fun show() {
|
||||
val targetValue = when {
|
||||
hasPartiallyExpandedState -> SheetValue.PartiallyExpanded
|
||||
else -> SheetValue.Expanded
|
||||
}
|
||||
animateTo(targetValue)
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide the bottom sheet with animation and suspend until it is fully hidden or animation has
|
||||
* been cancelled.
|
||||
* @throws [CancellationException] if the animation is interrupted
|
||||
*/
|
||||
suspend fun hide() {
|
||||
check(!skipHiddenState) {
|
||||
"Attempted to animate to hidden when skipHiddenState was enabled. Set skipHiddenState" +
|
||||
" to false to use this function."
|
||||
}
|
||||
animateTo(SheetValue.Hidden)
|
||||
}
|
||||
|
||||
/**
|
||||
* Animate to a [targetValue].
|
||||
* If the [targetValue] is not in the set of anchors, the [currentValue] will be updated to the
|
||||
* [targetValue] without updating the offset.
|
||||
*
|
||||
* @throws CancellationException if the interaction interrupted by another interaction like a
|
||||
* gesture interaction or another programmatic interaction like a [animateTo] or [snapTo] call.
|
||||
*
|
||||
* @param targetValue The target value of the animation
|
||||
*/
|
||||
internal suspend fun animateTo(
|
||||
targetValue: SheetValue,
|
||||
velocity: Float = anchoredDraggableState.lastVelocity,
|
||||
) {
|
||||
anchoredDraggableState.animateTo(targetValue, velocity)
|
||||
}
|
||||
|
||||
/**
|
||||
* Snap to a [targetValue] without any animation.
|
||||
*
|
||||
* @throws CancellationException if the interaction interrupted by another interaction like a
|
||||
* gesture interaction or another programmatic interaction like a [animateTo] or [snapTo] call.
|
||||
*
|
||||
* @param targetValue The target value of the animation
|
||||
*/
|
||||
internal suspend fun snapTo(targetValue: SheetValue) {
|
||||
anchoredDraggableState.snapTo(targetValue)
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the closest anchor taking into account the velocity and settle at it with an animation.
|
||||
*/
|
||||
internal suspend fun settle(velocity: Float) {
|
||||
anchoredDraggableState.settle(velocity)
|
||||
}
|
||||
|
||||
internal var anchoredDraggableState = AnchoredDraggableState(
|
||||
initialValue = initialValue,
|
||||
animationSpec = AnchoredDraggableDefaults2.AnimationSpec,
|
||||
confirmValueChange = confirmValueChange,
|
||||
positionalThreshold = { with(requireDensity()) { 56.dp.toPx() } },
|
||||
velocityThreshold = { with(requireDensity()) { 125.dp.toPx() } },
|
||||
)
|
||||
|
||||
internal val offset: Float get() = anchoredDraggableState.offset
|
||||
|
||||
internal var density: Density? = null
|
||||
private fun requireDensity() = requireNotNull(density) {
|
||||
"SheetState did not have a density attached. Are you using SheetState with " +
|
||||
"BottomSheetScaffold or ModalBottomSheet component?"
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* The default [Saver] implementation for [SheetState2].
|
||||
*/
|
||||
fun Saver(
|
||||
skipPartiallyExpanded: Boolean,
|
||||
confirmValueChange: (SheetValue) -> Boolean,
|
||||
density: Density,
|
||||
) = Saver<SheetState2, SheetValue>(
|
||||
save = { it.currentValue },
|
||||
restore = { savedValue ->
|
||||
SheetState2(skipPartiallyExpanded, density, savedValue, confirmValueChange)
|
||||
},
|
||||
)
|
||||
|
||||
/**
|
||||
* The default [Saver] implementation for [SheetState2].
|
||||
*/
|
||||
@Deprecated(
|
||||
message = "This function is deprecated. Please use the overload where Density is" +
|
||||
" provided.",
|
||||
replaceWith = ReplaceWith(
|
||||
"Saver(skipPartiallyExpanded, confirmValueChange, LocalDensity.current)",
|
||||
),
|
||||
)
|
||||
@Suppress("Deprecation")
|
||||
fun Saver(
|
||||
skipPartiallyExpanded: Boolean,
|
||||
confirmValueChange: (SheetValue) -> Boolean,
|
||||
) = Saver<SheetState2, SheetValue>(
|
||||
save = { it.currentValue },
|
||||
restore = { savedValue ->
|
||||
SheetState2(skipPartiallyExpanded, savedValue, confirmValueChange)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* <a href="https://m3.material.io/components/bottom-sheets/overview" class="external" target="_blank">Material Design modal bottom sheet</a>.
|
||||
*
|
||||
* Modal bottom sheets are used as an alternative to inline menus or simple dialogs on mobile,
|
||||
* especially when offering a long list of action items, or when items require longer descriptions
|
||||
* and icons. Like dialogs, modal bottom sheets appear in front of app content, disabling all other
|
||||
* app functionality when they appear, and remaining on screen until confirmed, dismissed, or a
|
||||
* required action has been taken.
|
||||
*
|
||||
* 
|
||||
*
|
||||
* A simple example of a modal bottom sheet looks like this:
|
||||
*
|
||||
* @sample androidx.compose.material3.samples.ModalBottomSheetSample
|
||||
*
|
||||
* @param onDismissRequest Executes when the user clicks outside of the bottom sheet, after sheet
|
||||
* animates to [Hidden].
|
||||
* @param modifier Optional [Modifier] for the bottom sheet.
|
||||
* @param sheetState The state of the bottom sheet.
|
||||
* @param sheetMaxWidth [Dp] that defines what the maximum width the sheet will take.
|
||||
* Pass in [Dp.Unspecified] for a sheet that spans the entire screen width.
|
||||
* @param shape The shape of the bottom sheet.
|
||||
* @param containerColor The color used for the background of this bottom sheet
|
||||
* @param contentColor The preferred color for content inside this bottom sheet. Defaults to either
|
||||
* the matching content color for [containerColor], or to the current [LocalContentColor] if
|
||||
* [containerColor] is not a color from the theme.
|
||||
* @param tonalElevation The tonal elevation of this bottom sheet.
|
||||
* @param scrimColor Color of the scrim that obscures content when the bottom sheet is open.
|
||||
* @param dragHandle Optional visual marker to swipe the bottom sheet.
|
||||
* @param windowInsets window insets to be passed to the bottom sheet window via [PaddingValues]
|
||||
* params.
|
||||
* @param properties [ModalBottomSheetProperties2] for further customization of this
|
||||
* modal bottom sheet's behavior.
|
||||
* @param content The content to be displayed inside the bottom sheet.
|
||||
*/
|
||||
@Composable
|
||||
@ExperimentalMaterial3Api
|
||||
fun ModalBottomSheetFix(
|
||||
onDismissRequest: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
sheetState: SheetState2 = rememberModalBottomSheetState(),
|
||||
sheetMaxWidth: Dp = BottomSheetDefaults.SheetMaxWidth,
|
||||
shape: Shape = BottomSheetDefaults.ExpandedShape,
|
||||
containerColor: Color = BottomSheetDefaults.ContainerColor,
|
||||
contentColor: Color = contentColorFor(containerColor),
|
||||
tonalElevation: Dp = BottomSheetDefaults.Elevation,
|
||||
scrimColor: Color = BottomSheetDefaults.ScrimColor,
|
||||
dragHandle: @Composable (() -> Unit)? = { BottomSheetDefaults.DragHandle() },
|
||||
windowInsets: WindowInsets = BottomSheetDefaults.windowInsets,
|
||||
properties: ModalBottomSheetProperties2 = ModalBottomSheetDefaults2.properties(),
|
||||
content: @Composable ColumnScope.() -> Unit,
|
||||
) {
|
||||
// b/291735717 Remove this once deprecated methods without density are removed
|
||||
val density = LocalDensity.current
|
||||
SideEffect {
|
||||
sheetState.density = density
|
||||
}
|
||||
val scope = rememberCoroutineScope()
|
||||
val animateToDismiss: () -> Unit = {
|
||||
if (sheetState.confirmValueChange(SheetValue.Hidden)) {
|
||||
scope.launch { sheetState.hide() }.invokeOnCompletion {
|
||||
if (!sheetState.isVisible) {
|
||||
onDismissRequest()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
LaunchedEffect(sheetState) {
|
||||
snapshotFlow {
|
||||
sheetState.targetValue == SheetValue.Hidden && !sheetState.isVisible
|
||||
}.drop(1).collectLatest {
|
||||
if (it) onDismissRequest()
|
||||
}
|
||||
}
|
||||
val settleToDismiss: (velocity: Float) -> Unit = {
|
||||
scope.launch { sheetState.settle(it) }.invokeOnCompletion {
|
||||
if (!sheetState.isVisible) onDismissRequest()
|
||||
}
|
||||
}
|
||||
|
||||
val popupOnDismissRequest: () -> Unit = {
|
||||
if (sheetState.currentValue == SheetValue.Expanded && sheetState.hasPartiallyExpandedState) {
|
||||
scope.launch { sheetState.partialExpand() }
|
||||
} else { // Is expanded without collapsed state or is collapsed.
|
||||
scope.launch { sheetState.hide() }.invokeOnCompletion { onDismissRequest() }
|
||||
}
|
||||
}
|
||||
|
||||
ModalBottomSheetPopup(
|
||||
properties = properties,
|
||||
onDismissRequest = popupOnDismissRequest,
|
||||
windowInsets = windowInsets,
|
||||
) {
|
||||
val configuration = LocalConfiguration.current
|
||||
val widthDp = configuration.screenWidthDp.dp
|
||||
val height = configuration.screenHeightDp.dp
|
||||
val predictiveState by animateFloatMergeOneWayPredictiveBackAsState(true) {
|
||||
sheetState.hide()
|
||||
onDismissRequest()
|
||||
}
|
||||
val predictiveModifier = Modifier.graphicsLayer {
|
||||
val scale = lerp(1f, 0.90f, predictiveState.absoluteValue)
|
||||
scaleX = scale
|
||||
scaleY = scale
|
||||
}.offset {
|
||||
val ofsY = lerp(0f, height.toPx() * 0.0625f, predictiveState.absoluteValue).roundToInt()
|
||||
val ofsX = lerp(0f, widthDp.toPx() * 0.025f, predictiveState).roundToInt()
|
||||
IntOffset(ofsX, ofsY)
|
||||
}
|
||||
BoxWithConstraints(Modifier.fillMaxSize()) {
|
||||
val fullHeight = constraints.maxHeight
|
||||
Scrim(
|
||||
color = scrimColor,
|
||||
onDismissRequest = animateToDismiss,
|
||||
visible = sheetState.targetValue != SheetValue.Hidden,
|
||||
)
|
||||
Surface(
|
||||
modifier = modifier
|
||||
.widthIn(max = sheetMaxWidth)
|
||||
.fillMaxWidth()
|
||||
.align(Alignment.TopCenter)
|
||||
.offset {
|
||||
IntOffset(
|
||||
0,
|
||||
sheetState
|
||||
.requireOffset()
|
||||
.toInt(),
|
||||
)
|
||||
} then predictiveModifier
|
||||
.nestedScroll(
|
||||
remember(sheetState) {
|
||||
ConsumeSwipeWithinBottomSheetBoundsNestedScrollConnection(
|
||||
sheetState = sheetState,
|
||||
orientation = Orientation.Vertical,
|
||||
onFling = settleToDismiss,
|
||||
)
|
||||
},
|
||||
)
|
||||
.anchoredDraggable(
|
||||
state = sheetState.anchoredDraggableState,
|
||||
orientation = Orientation.Vertical,
|
||||
enabled = sheetState.isVisible,
|
||||
startDragImmediately = sheetState.anchoredDraggableState.isAnimationRunning,
|
||||
)
|
||||
.modalBottomSheetAnchors(
|
||||
sheetState = sheetState,
|
||||
fullHeight = fullHeight.toFloat(),
|
||||
),
|
||||
shape = shape,
|
||||
color = containerColor,
|
||||
contentColor = contentColor,
|
||||
tonalElevation = tonalElevation,
|
||||
) {
|
||||
Column(Modifier.fillMaxWidth()) {
|
||||
if (dragHandle != null) {
|
||||
Box(Modifier.align(Alignment.CenterHorizontally)) {
|
||||
dragHandle()
|
||||
}
|
||||
}
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (sheetState.hasExpandedState) {
|
||||
LaunchedEffect(sheetState) {
|
||||
sheetState.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
internal fun ConsumeSwipeWithinBottomSheetBoundsNestedScrollConnection(
|
||||
sheetState: SheetState2,
|
||||
orientation: Orientation,
|
||||
onFling: (velocity: Float) -> Unit,
|
||||
): NestedScrollConnection = object : NestedScrollConnection {
|
||||
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
|
||||
val delta = available.toFloat()
|
||||
return if (delta < 0 && source == NestedScrollSource.Drag) {
|
||||
sheetState.anchoredDraggableState.dispatchRawDelta(delta).toOffset()
|
||||
} else {
|
||||
Offset.Zero
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPostScroll(
|
||||
consumed: Offset,
|
||||
available: Offset,
|
||||
source: NestedScrollSource,
|
||||
): Offset {
|
||||
return if (source == NestedScrollSource.Drag) {
|
||||
sheetState.anchoredDraggableState.dispatchRawDelta(available.toFloat()).toOffset()
|
||||
} else {
|
||||
Offset.Zero
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun onPreFling(available: Velocity): Velocity {
|
||||
val toFling = available.toFloat()
|
||||
val currentOffset = sheetState.requireOffset()
|
||||
val minAnchor = sheetState.anchoredDraggableState.anchors.minAnchor()
|
||||
return if (toFling < 0 && currentOffset > minAnchor) {
|
||||
onFling(toFling)
|
||||
// since we go to the anchor with tween settling, consume all for the best UX
|
||||
available
|
||||
} else {
|
||||
Velocity.Zero
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
|
||||
onFling(available.toFloat())
|
||||
return available
|
||||
}
|
||||
|
||||
private fun Float.toOffset(): Offset = Offset(
|
||||
x = if (orientation == Orientation.Horizontal) this else 0f,
|
||||
y = if (orientation == Orientation.Vertical) this else 0f,
|
||||
)
|
||||
|
||||
@JvmName("velocityToFloat")
|
||||
private fun Velocity.toFloat() = if (orientation == Orientation.Horizontal) x else y
|
||||
|
||||
@JvmName("offsetToFloat")
|
||||
private fun Offset.toFloat(): Float = if (orientation == Orientation.Horizontal) x else y
|
||||
}
|
||||
|
||||
/**
|
||||
* Properties used to customize the behavior of a [ModalBottomSheetFix].
|
||||
*
|
||||
* @param securePolicy Policy for setting [WindowManager.LayoutParams.FLAG_SECURE] on the bottom
|
||||
* sheet's window.
|
||||
* @param isFocusable Whether the modal bottom sheet is focusable. When true,
|
||||
* the modal bottom sheet will receive IME events and key presses, such as when
|
||||
* the back button is pressed.
|
||||
* @param shouldDismissOnBackPress Whether the modal bottom sheet can be dismissed by pressing
|
||||
* the back button. If true, pressing the back button will call onDismissRequest.
|
||||
* Note that [isFocusable] must be set to true in order to receive key events such as
|
||||
* the back button - if the modal bottom sheet is not focusable then this property does nothing.
|
||||
*/
|
||||
@ExperimentalMaterial3Api
|
||||
class ModalBottomSheetProperties2(
|
||||
val securePolicy: SecureFlagPolicy,
|
||||
val isFocusable: Boolean,
|
||||
val shouldDismissOnBackPress: Boolean,
|
||||
) {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is ModalBottomSheetProperties2) return false
|
||||
|
||||
if (securePolicy != other.securePolicy) return false
|
||||
if (isFocusable != other.isFocusable) return false
|
||||
if (shouldDismissOnBackPress != other.shouldDismissOnBackPress) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = securePolicy.hashCode()
|
||||
result = 31 * result + isFocusable.hashCode()
|
||||
result = 31 * result + shouldDismissOnBackPress.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Default values for [ModalBottomSheetFix]
|
||||
*/
|
||||
@Immutable
|
||||
@ExperimentalMaterial3Api
|
||||
object ModalBottomSheetDefaults2 {
|
||||
/**
|
||||
* Properties used to customize the behavior of a [ModalBottomSheetFix].
|
||||
*
|
||||
* @param securePolicy Policy for setting [WindowManager.LayoutParams.FLAG_SECURE] on the bottom
|
||||
* sheet's window.
|
||||
* @param isFocusable Whether the modal bottom sheet is focusable. When true,
|
||||
* the modal bottom sheet will receive IME events and key presses, such as when
|
||||
* the back button is pressed.
|
||||
* @param shouldDismissOnBackPress Whether the modal bottom sheet can be dismissed by pressing
|
||||
* the back button. If true, pressing the back button will call onDismissRequest.
|
||||
* Note that [isFocusable] must be set to true in order to receive key events such as
|
||||
* the back button - if the modal bottom sheet is not focusable then this property does nothing.
|
||||
*/
|
||||
fun properties(
|
||||
securePolicy: SecureFlagPolicy = SecureFlagPolicy.Inherit,
|
||||
isFocusable: Boolean = true,
|
||||
shouldDismissOnBackPress: Boolean = true,
|
||||
) = ModalBottomSheetProperties2(securePolicy, isFocusable, shouldDismissOnBackPress)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and [remember] a [SheetState2] for [ModalBottomSheetFix].
|
||||
*
|
||||
* @param skipPartiallyExpanded Whether the partially expanded state, if the sheet is tall enough,
|
||||
* should be skipped. If true, the sheet will always expand to the [Expanded] state and move to the
|
||||
* [Hidden] state when hiding the sheet, either programmatically or by user interaction.
|
||||
* @param confirmValueChange Optional callback invoked to confirm or veto a pending state change.
|
||||
*/
|
||||
@Composable
|
||||
@ExperimentalMaterial3Api
|
||||
fun rememberModalBottomSheetState(
|
||||
skipPartiallyExpanded: Boolean = false,
|
||||
confirmValueChange: (SheetValue) -> Boolean = { true },
|
||||
) = rememberSheetState(skipPartiallyExpanded, confirmValueChange, SheetValue.Hidden)
|
||||
|
||||
@Composable
|
||||
@ExperimentalMaterial3Api
|
||||
internal fun rememberSheetState(
|
||||
skipPartiallyExpanded: Boolean = false,
|
||||
confirmValueChange: (SheetValue) -> Boolean = { true },
|
||||
initialValue: SheetValue = SheetValue.Hidden,
|
||||
skipHiddenState: Boolean = false,
|
||||
): SheetState2 {
|
||||
val density = LocalDensity.current
|
||||
return rememberSaveable(
|
||||
skipPartiallyExpanded,
|
||||
confirmValueChange,
|
||||
saver = SheetState2.Saver(
|
||||
skipPartiallyExpanded = skipPartiallyExpanded,
|
||||
confirmValueChange = confirmValueChange,
|
||||
density = density,
|
||||
),
|
||||
) {
|
||||
SheetState2(
|
||||
skipPartiallyExpanded,
|
||||
density,
|
||||
initialValue,
|
||||
confirmValueChange,
|
||||
skipHiddenState,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Scrim(
|
||||
color: Color,
|
||||
onDismissRequest: () -> Unit,
|
||||
visible: Boolean,
|
||||
) {
|
||||
if (color.isSpecified) {
|
||||
val alpha by animateFloatAsState(
|
||||
targetValue = if (visible) 1f else 0f,
|
||||
animationSpec = TweenSpec(),
|
||||
label = "scrim",
|
||||
)
|
||||
val dismissSheet = if (visible) {
|
||||
Modifier
|
||||
.pointerInput(onDismissRequest) {
|
||||
detectTapGestures {
|
||||
onDismissRequest()
|
||||
}
|
||||
}
|
||||
.clearAndSetSemantics {}
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
Canvas(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.then(dismissSheet),
|
||||
) {
|
||||
drawRect(color = color, alpha = alpha)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ExperimentalMaterial3Api
|
||||
private fun Modifier.modalBottomSheetAnchors(
|
||||
sheetState: SheetState2,
|
||||
fullHeight: Float,
|
||||
) = onSizeChanged { sheetSize ->
|
||||
|
||||
val newAnchors = DraggableAnchors {
|
||||
SheetValue.Hidden at fullHeight
|
||||
if (sheetSize.height > (fullHeight / 2) && !sheetState.skipPartiallyExpanded) {
|
||||
SheetValue.PartiallyExpanded at fullHeight / 2f
|
||||
}
|
||||
if (sheetSize.height != 0) {
|
||||
SheetValue.Expanded at max(0f, fullHeight - sheetSize.height)
|
||||
}
|
||||
}
|
||||
|
||||
val newTarget = when (sheetState.anchoredDraggableState.targetValue) {
|
||||
SheetValue.Hidden -> SheetValue.Hidden
|
||||
SheetValue.PartiallyExpanded, SheetValue.Expanded -> {
|
||||
val hasPartiallyExpandedState = newAnchors.hasAnchorFor(SheetValue.PartiallyExpanded)
|
||||
val newTarget = if (hasPartiallyExpandedState) {
|
||||
SheetValue.PartiallyExpanded
|
||||
} else if (newAnchors.hasAnchorFor(SheetValue.Expanded)) SheetValue.Expanded else SheetValue.Hidden
|
||||
newTarget
|
||||
}
|
||||
}
|
||||
|
||||
sheetState.anchoredDraggableState.updateAnchors(newAnchors, newTarget)
|
||||
}
|
||||
|
||||
/**
|
||||
* Popup specific for modal bottom sheet.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
internal fun ModalBottomSheetPopup(
|
||||
properties: ModalBottomSheetProperties2,
|
||||
onDismissRequest: () -> Unit,
|
||||
windowInsets: WindowInsets,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
val view = LocalView.current
|
||||
val id = rememberSaveable { UUID.randomUUID() }
|
||||
val parentComposition = rememberCompositionContext()
|
||||
val currentContent by rememberUpdatedState(content)
|
||||
val layoutDirection = LocalLayoutDirection.current
|
||||
val configuration = LocalConfiguration.current
|
||||
val modalBottomSheetWindow = remember(configuration) {
|
||||
ModalBottomSheetWindow2(
|
||||
properties = properties,
|
||||
onDismissRequest = onDismissRequest,
|
||||
composeView = view,
|
||||
saveId = id,
|
||||
).apply {
|
||||
setCustomContent(
|
||||
parent = parentComposition,
|
||||
content = {
|
||||
Box(
|
||||
Modifier
|
||||
.semantics { this.popup() }
|
||||
.windowInsetsPadding(windowInsets)
|
||||
.then(
|
||||
// TODO(b/290893168): Figure out a solution for APIs < 30.
|
||||
if (Build.VERSION.SDK_INT >= 33) {
|
||||
Modifier.imePadding()
|
||||
} else {
|
||||
Modifier
|
||||
},
|
||||
),
|
||||
) {
|
||||
currentContent()
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
DisposableEffect(modalBottomSheetWindow) {
|
||||
modalBottomSheetWindow.show()
|
||||
modalBottomSheetWindow.superSetLayoutDirection(layoutDirection)
|
||||
onDispose {
|
||||
modalBottomSheetWindow.disposeComposition()
|
||||
modalBottomSheetWindow.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Custom compose view for [ModalBottomSheetFix] */
|
||||
@SuppressLint("ViewConstructor")
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
private class ModalBottomSheetWindow2(
|
||||
private val properties: ModalBottomSheetProperties2,
|
||||
private var onDismissRequest: () -> Unit,
|
||||
private val composeView: View,
|
||||
saveId: UUID,
|
||||
) : AbstractComposeView(composeView.context),
|
||||
ViewTreeObserver.OnGlobalLayoutListener,
|
||||
ViewRootForInspector,
|
||||
OnBackPressedDispatcherOwner {
|
||||
|
||||
init {
|
||||
id = android.R.id.content
|
||||
// Set up view owners
|
||||
setViewTreeLifecycleOwner(composeView.findViewTreeLifecycleOwner())
|
||||
setViewTreeViewModelStoreOwner(composeView.findViewTreeViewModelStoreOwner())
|
||||
setViewTreeSavedStateRegistryOwner(composeView.findViewTreeSavedStateRegistryOwner())
|
||||
setTag(androidx.compose.ui.R.id.compose_view_saveable_id_tag, "Popup:$saveId")
|
||||
// Enable children to draw their shadow by not clipping them
|
||||
clipChildren = false
|
||||
}
|
||||
|
||||
override val lifecycle = composeView.findViewTreeLifecycleOwner()!!.lifecycle
|
||||
override val onBackPressedDispatcher = OnBackPressedDispatcher {
|
||||
onDismissRequest()
|
||||
}
|
||||
|
||||
private val windowManager =
|
||||
composeView.context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
|
||||
|
||||
private val displayWidth: Int
|
||||
get() = context.resources.displayMetrics.widthPixels
|
||||
|
||||
private val params: WindowManager.LayoutParams =
|
||||
WindowManager.LayoutParams().apply {
|
||||
// Position bottom sheet from the bottom of the screen
|
||||
gravity = Gravity.BOTTOM or Gravity.START
|
||||
// Application panel window
|
||||
type = WindowManager.LayoutParams.TYPE_APPLICATION_PANEL
|
||||
// Fill up the entire app view
|
||||
width = displayWidth
|
||||
height = WindowManager.LayoutParams.MATCH_PARENT
|
||||
|
||||
// Format of screen pixels
|
||||
format = PixelFormat.TRANSLUCENT
|
||||
// Title used as fallback for a11y services
|
||||
// TODO: Provide bottom sheet window resource
|
||||
title = composeView.context.resources.getString(
|
||||
androidx.compose.ui.R.string.default_popup_window_title,
|
||||
)
|
||||
// Get the Window token from the parent view
|
||||
token = composeView.applicationWindowToken
|
||||
|
||||
// Flags specific to modal bottom sheet.
|
||||
flags = flags and (
|
||||
WindowManager.LayoutParams.FLAG_IGNORE_CHEEK_PRESSES or
|
||||
WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM
|
||||
).inv()
|
||||
|
||||
flags = flags or WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS
|
||||
|
||||
// Security flag
|
||||
val secureFlagEnabled =
|
||||
properties.securePolicy.shouldApplySecureFlag(composeView.isFlagSecureEnabled())
|
||||
if (secureFlagEnabled) {
|
||||
flags = flags or WindowManager.LayoutParams.FLAG_SECURE
|
||||
} else {
|
||||
flags = flags and (WindowManager.LayoutParams.FLAG_SECURE.inv())
|
||||
}
|
||||
|
||||
// Focusable
|
||||
if (!properties.isFocusable) {
|
||||
flags = flags or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
|
||||
} else {
|
||||
flags = flags and (WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE.inv())
|
||||
}
|
||||
}
|
||||
|
||||
private var content: @Composable () -> Unit by mutableStateOf({})
|
||||
|
||||
override var shouldCreateCompositionOnAttachedToWindow: Boolean = false
|
||||
private set
|
||||
|
||||
@Composable
|
||||
override fun Content() {
|
||||
CompositionLocalProvider(LocalOnBackPressedDispatcherOwner provides this) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
fun setCustomContent(
|
||||
parent: CompositionContext? = null,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
parent?.let { setParentCompositionContext(it) }
|
||||
this.content = content
|
||||
shouldCreateCompositionOnAttachedToWindow = true
|
||||
}
|
||||
|
||||
fun show() {
|
||||
windowManager.addView(this, params)
|
||||
if (isAtLeastT) {
|
||||
onBackPressedDispatcher.setOnBackInvokedDispatcher(findOnBackInvokedDispatcher()!!)
|
||||
}
|
||||
}
|
||||
|
||||
fun dismiss() {
|
||||
setViewTreeLifecycleOwner(null)
|
||||
setViewTreeSavedStateRegistryOwner(null)
|
||||
composeView.viewTreeObserver.removeOnGlobalLayoutListener(this)
|
||||
windowManager.removeViewImmediate(this)
|
||||
}
|
||||
|
||||
override fun onGlobalLayout() {
|
||||
// No-op
|
||||
}
|
||||
|
||||
override fun setLayoutDirection(layoutDirection: Int) {
|
||||
// Do nothing. ViewRootImpl will call this method attempting to set the layout direction
|
||||
// from the context's locale, but we have one already from the parent composition.
|
||||
}
|
||||
|
||||
// Sets the "real" layout direction for our content that we obtain from the parent composition.
|
||||
fun superSetLayoutDirection(layoutDirection: LayoutDirection) {
|
||||
val direction = when (layoutDirection) {
|
||||
LayoutDirection.Ltr -> android.util.LayoutDirection.LTR
|
||||
LayoutDirection.Rtl -> android.util.LayoutDirection.RTL
|
||||
}
|
||||
super.setLayoutDirection(direction)
|
||||
}
|
||||
}
|
||||
|
||||
// Taken from AndroidPopup.android.kt
|
||||
private fun View.isFlagSecureEnabled(): Boolean {
|
||||
val windowParams = rootView.layoutParams as? WindowManager.LayoutParams
|
||||
if (windowParams != null) {
|
||||
return (windowParams.flags and WindowManager.LayoutParams.FLAG_SECURE) != 0
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Taken from AndroidPopup.android.kt
|
||||
private fun SecureFlagPolicy.shouldApplySecureFlag(isSecureFlagSetOnParent: Boolean): Boolean {
|
||||
return when (this) {
|
||||
SecureFlagPolicy.SecureOff -> false
|
||||
SecureFlagPolicy.SecureOn -> true
|
||||
SecureFlagPolicy.Inherit -> isSecureFlagSetOnParent
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
@file:Suppress("ktlint:standard:property-naming")
|
||||
|
||||
package androidx.compose.material3
|
||||
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.interaction.collectIsFocusedAsState
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.sizeIn
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.text.input.TextFieldLineLimits
|
||||
import androidx.compose.foundation.text.input.TextFieldState
|
||||
import androidx.compose.material3.SearchBarDefaults.InputFieldHeight
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.graphics.takeOrElse
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
@Composable
|
||||
fun SearchBarInputField(
|
||||
state: TextFieldState,
|
||||
onSearch: () -> Unit,
|
||||
expanded: Boolean,
|
||||
onExpandedChange: (Boolean) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
label: @Composable (() -> Unit)? = null,
|
||||
placeholder: @Composable (() -> Unit)? = null,
|
||||
leadingIcon: @Composable (() -> Unit)? = null,
|
||||
trailingIcon: @Composable (() -> Unit)? = null,
|
||||
colors: TextFieldColors = SearchBarDefaults.inputFieldColors(),
|
||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||
) {
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
val focusManager = LocalFocusManager.current
|
||||
val focused by interactionSource.collectIsFocusedAsState()
|
||||
val textColor = LocalTextStyle.current.color.takeOrElse {
|
||||
if (focused) colors.focusedTextColor else colors.unfocusedTextColor
|
||||
}
|
||||
|
||||
BasicTextField(
|
||||
state = state,
|
||||
modifier = modifier.sizeIn(
|
||||
minWidth = SearchBarMinWidth,
|
||||
maxWidth = SearchBarMaxWidth,
|
||||
minHeight = InputFieldHeight,
|
||||
).focusRequester(focusRequester).onFocusChanged {
|
||||
if (it.isFocused) onExpandedChange(true)
|
||||
}.fillMaxWidth(),
|
||||
enabled = enabled,
|
||||
lineLimits = TextFieldLineLimits.SingleLine,
|
||||
textStyle = LocalTextStyle.current.merge(TextStyle(color = textColor)),
|
||||
cursorBrush = SolidColor(colors.cursorColor),
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
|
||||
onKeyboardAction = { onSearch() },
|
||||
interactionSource = interactionSource,
|
||||
decorator = @Composable { innerTextField ->
|
||||
TextFieldDefaults.DecorationBox(
|
||||
value = state.text.toString(),
|
||||
innerTextField = innerTextField,
|
||||
enabled = enabled,
|
||||
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.contentPaddingWithLabel(),
|
||||
container = {},
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
val shouldClearFocus = !expanded && focused
|
||||
LaunchedEffect(expanded) {
|
||||
if (shouldClearFocus) {
|
||||
// Not strictly needed according to the motion spec, but since the animation
|
||||
// already has a delay, this works around b/261632544.
|
||||
delay(AnimationDelayMillis.toLong())
|
||||
focusManager.clearFocus()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val SearchBarIconOffsetX = 4.dp
|
||||
val SearchBarMinWidth = 360.dp
|
||||
val SearchBarMaxWidth = 720.dp
|
||||
val SearchBarHorizontalPadding = 16.dp
|
||||
const val AnimationDelayMillis = 100
|
||||
@@ -1,502 +0,0 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
@file:Suppress("ktlint:standard:property-naming")
|
||||
|
||||
package androidx.compose.material3
|
||||
|
||||
import androidx.activity.BackEventCompat
|
||||
import androidx.activity.compose.PredictiveBackHandler
|
||||
import androidx.compose.animation.core.Animatable
|
||||
import androidx.compose.animation.core.CubicBezierEasing
|
||||
import androidx.compose.animation.core.EaseOut
|
||||
import androidx.compose.animation.core.FiniteAnimationSpec
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.MutatorMutex
|
||||
import androidx.compose.foundation.interaction.Interaction
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.interaction.collectIsFocusedAsState
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.MutableWindowInsets
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.asPaddingValues
|
||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||
import androidx.compose.foundation.layout.exclude
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.onConsumedWindowInsetsChanged
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.GenericShape
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.text2.BasicTextField2
|
||||
import androidx.compose.foundation.text2.input.TextFieldLineLimits
|
||||
import androidx.compose.foundation.text2.input.TextFieldState
|
||||
import androidx.compose.material3.SearchBarDefaults.InputFieldHeight
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.structuralEqualityPolicy
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.geometry.CornerRadius
|
||||
import androidx.compose.ui.geometry.RoundRect
|
||||
import androidx.compose.ui.geometry.toRect
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.graphics.takeOrElse
|
||||
import androidx.compose.ui.layout.layout
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.semantics.onClick
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.unit.Constraints
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.lerp
|
||||
import androidx.compose.ui.unit.offset
|
||||
import androidx.compose.ui.util.lerp
|
||||
import androidx.compose.ui.zIndex
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
import kotlin.math.roundToInt
|
||||
import kotlin.math.sign
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
/**
|
||||
* <a href="https://m3.material.io/components/search/overview" class="external" target="_blank">Material Design search</a>.
|
||||
*
|
||||
* A search bar represents a floating search field that allows users to enter a keyword or phrase
|
||||
* and get relevant information. It can be used as a way to navigate through an app via search
|
||||
* queries.
|
||||
*
|
||||
* An active search bar expands into a search "view" and can be used to display dynamic suggestions.
|
||||
*
|
||||
* 
|
||||
*
|
||||
* A [SearchBar] expands to occupy the entirety of its allowed size when active. For full-screen
|
||||
* behavior as specified by Material guidelines, parent layouts of the [SearchBar] must not pass
|
||||
* any [Constraints] that limit its size, and the host activity should set
|
||||
* `WindowCompat.setDecorFitsSystemWindows(window, false)`.
|
||||
*
|
||||
* If this expansion behavior is undesirable, for example on large tablet screens, [DockedSearchBar]
|
||||
* can be used instead.
|
||||
*
|
||||
* @param state the query text to be shown in the search bar's input field
|
||||
* @param onSearch the callback to be invoked when the input service triggers the [ImeAction.Search]
|
||||
* action.
|
||||
* @param active whether this search bar is active
|
||||
* @param onActiveChange the callback to be invoked when this search bar's active state is changed
|
||||
* @param modifier the [Modifier] to be applied to this search bar
|
||||
* @param enabled controls the enabled state of this search bar. When `false`, this component will
|
||||
* not respond to user input, and it will appear visually disabled and disabled to accessibility
|
||||
* services.
|
||||
* @param placeholder the placeholder to be displayed when the search bar's query is empty.
|
||||
* @param leadingIcon the leading icon to be displayed at the beginning of the search bar container
|
||||
* @param trailingIcon the trailing icon to be displayed at the end of the search bar container
|
||||
* @param shape the shape of this search bar when it is not [active]. When [active], the shape will
|
||||
* always be [SearchBarDefaults.fullScreenShape].
|
||||
* @param colors [SearchBarColors] that will be used to resolve the colors used for this search bar
|
||||
* in different states. See [SearchBarDefaults.colors].
|
||||
* @param tonalElevation when [SearchBarColors.containerColor] is [ColorScheme.surface], a
|
||||
* translucent primary color overlay is applied on top of the container. A higher tonal elevation
|
||||
* value will result in a darker color in light theme and lighter color in dark theme. See also:
|
||||
* [Surface].
|
||||
* @param shadowElevation the elevation for the shadow below the search bar
|
||||
* @param windowInsets the window insets that the search bar will respect
|
||||
* @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s
|
||||
* for this search bar. You can create and pass in your own `remember`ed instance to observe
|
||||
* [Interaction]s and customize the appearance / behavior of this search bar in different states.
|
||||
* @param content the content of this search bar that will be displayed below the input field
|
||||
*/
|
||||
@ExperimentalMaterial3Api
|
||||
@Composable
|
||||
fun SearchBar(
|
||||
state: TextFieldState,
|
||||
onSearch: () -> Unit,
|
||||
active: Boolean,
|
||||
onActiveChange: (Boolean) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
title: @Composable (() -> Unit)? = null,
|
||||
placeholder: @Composable (() -> Unit)? = null,
|
||||
leadingIcon: @Composable (() -> Unit)? = null,
|
||||
trailingIcon: @Composable (() -> Unit)? = null,
|
||||
shape: Shape = SearchBarDefaults.inputFieldShape,
|
||||
colors: SearchBarColors = SearchBarDefaults.colors(),
|
||||
tonalElevation: Dp = SearchBarDefaults.TonalElevation,
|
||||
shadowElevation: Dp = SearchBarDefaults.ShadowElevation,
|
||||
windowInsets: WindowInsets = SearchBarDefaults.windowInsets,
|
||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||
content: @Composable ColumnScope.() -> Unit,
|
||||
) {
|
||||
val animationProgress = remember { Animatable(initialValue = if (active) 1f else 0f) }
|
||||
var firstBackEvent by remember { mutableStateOf<BackEventCompat?>(null) }
|
||||
var currentBackEvent by remember { mutableStateOf<BackEventCompat?>(null) }
|
||||
|
||||
val focusManager = LocalFocusManager.current
|
||||
val density = LocalDensity.current
|
||||
|
||||
val defaultInputFieldShape = SearchBarDefaults.inputFieldShape
|
||||
val defaultFullScreenShape = SearchBarDefaults.fullScreenShape
|
||||
val useFullScreenShape by remember {
|
||||
derivedStateOf(structuralEqualityPolicy()) { animationProgress.value == 1f }
|
||||
}
|
||||
val animatedShape = remember(useFullScreenShape, shape) {
|
||||
when {
|
||||
shape == defaultInputFieldShape ->
|
||||
// The shape can only be animated if it's the default spec value
|
||||
GenericShape { size, _ ->
|
||||
val radius = with(density) {
|
||||
(SearchBarCornerRadius * (1 - animationProgress.value)).toPx()
|
||||
}
|
||||
addRoundRect(RoundRect(size.toRect(), CornerRadius(radius)))
|
||||
}
|
||||
useFullScreenShape -> defaultFullScreenShape
|
||||
else -> shape
|
||||
}
|
||||
}
|
||||
|
||||
// The main animation complexity is allowing the component to smoothly expand while keeping the
|
||||
// input field at the same relative location on screen. `Modifier.windowInsetsPadding` does not
|
||||
// support animation and thus is not suitable. Instead, we convert the insets to a padding
|
||||
// applied to the Surface, which gradually becomes padding applied to the input field as the
|
||||
// animation proceeds.
|
||||
val unconsumedInsets = remember { MutableWindowInsets() }
|
||||
val topPadding = remember(density) {
|
||||
derivedStateOf {
|
||||
SearchBarVerticalPadding +
|
||||
unconsumedInsets.asPaddingValues(density).calculateTopPadding()
|
||||
}
|
||||
}
|
||||
|
||||
Surface(
|
||||
shape = animatedShape,
|
||||
color = colors.containerColor,
|
||||
contentColor = contentColorFor(colors.containerColor),
|
||||
tonalElevation = tonalElevation,
|
||||
shadowElevation = shadowElevation,
|
||||
modifier = modifier
|
||||
.padding(horizontal = lerp(SearchBarHorizontalPadding, 0.dp, animationProgress.value))
|
||||
.zIndex(1f)
|
||||
.onConsumedWindowInsetsChanged { consumedInsets ->
|
||||
unconsumedInsets.insets = windowInsets.exclude(consumedInsets)
|
||||
}
|
||||
.consumeWindowInsets(unconsumedInsets)
|
||||
.layout { measurable, constraints ->
|
||||
val animatedTopPadding =
|
||||
lerp(topPadding.value, 0.dp, animationProgress.value).roundToPx()
|
||||
|
||||
val startWidth: Int
|
||||
val startHeight: Int
|
||||
if (currentBackEvent == null) {
|
||||
startWidth = max(constraints.minWidth, SearchBarMinWidth.roundToPx())
|
||||
.coerceAtMost(min(constraints.maxWidth, SearchBarMaxWidth.roundToPx()))
|
||||
startHeight = max(constraints.minHeight, InputFieldHeight.roundToPx())
|
||||
.coerceAtMost(constraints.maxHeight)
|
||||
} else {
|
||||
startWidth =
|
||||
(constraints.maxWidth * SearchBarPredictiveBackMinScale).roundToInt()
|
||||
startHeight =
|
||||
(constraints.maxHeight * SearchBarPredictiveBackMinScale).roundToInt()
|
||||
}
|
||||
val endWidth = constraints.maxWidth
|
||||
val endHeight = constraints.maxHeight
|
||||
|
||||
val width = lerp(startWidth, endWidth, animationProgress.value)
|
||||
val height =
|
||||
lerp(startHeight, endHeight, animationProgress.value) + animatedTopPadding
|
||||
|
||||
val minOffsetMargin = SearchBarPredictiveBackMinMargin.roundToPx()
|
||||
val predictiveBackOffsetX = calculatePredictiveBackOffsetX(
|
||||
constraints,
|
||||
minOffsetMargin,
|
||||
currentBackEvent,
|
||||
animationProgress.value,
|
||||
)
|
||||
val predictiveBackOffsetY = calculatePredictiveBackOffsetY(
|
||||
constraints,
|
||||
minOffsetMargin,
|
||||
currentBackEvent,
|
||||
firstBackEvent,
|
||||
height,
|
||||
SearchBarPredictiveBackMaxOffsetY.roundToPx(),
|
||||
)
|
||||
val placeable = measurable.measure(
|
||||
Constraints
|
||||
.fixed(width, height)
|
||||
.offset(
|
||||
vertical = -animatedTopPadding,
|
||||
),
|
||||
)
|
||||
layout(width, height) {
|
||||
placeable.placeRelative(
|
||||
predictiveBackOffsetX,
|
||||
animatedTopPadding + predictiveBackOffsetY,
|
||||
)
|
||||
}
|
||||
},
|
||||
) {
|
||||
Column {
|
||||
val animatedInputFieldPadding = remember {
|
||||
AnimatedPaddingValues2(animationProgress.asState(), topPadding)
|
||||
}
|
||||
SearchBarInputField(
|
||||
state = state,
|
||||
onSearch = onSearch,
|
||||
onActiveChange = onActiveChange,
|
||||
modifier = Modifier.padding(paddingValues = animatedInputFieldPadding),
|
||||
enabled = enabled,
|
||||
label = title.takeUnless { active || animationProgress.value > 0 || state.text.isNotEmpty() },
|
||||
placeholder = placeholder,
|
||||
leadingIcon = leadingIcon,
|
||||
trailingIcon = trailingIcon,
|
||||
colors = colors.inputFieldColors,
|
||||
interactionSource = interactionSource,
|
||||
)
|
||||
|
||||
val showResults by remember {
|
||||
derivedStateOf(structuralEqualityPolicy()) { animationProgress.value > 0 }
|
||||
}
|
||||
if (showResults) {
|
||||
Column(Modifier.graphicsLayer { alpha = animationProgress.value }) {
|
||||
HorizontalDivider(color = colors.dividerColor)
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val isFocused = interactionSource.collectIsFocusedAsState().value
|
||||
val shouldClearFocus = !active && isFocused
|
||||
if (shouldClearFocus) {
|
||||
LaunchedEffect(true) {
|
||||
// Not strictly needed according to the motion spec, but since the animation already has
|
||||
// a delay, this works around b/261632544.
|
||||
delay(AnimationDelayMillis.toLong())
|
||||
focusManager.clearFocus()
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(active) {
|
||||
val animationInProgress = animationProgress.value > 0 && animationProgress.value < 1
|
||||
val animationSpec =
|
||||
if (animationInProgress) {
|
||||
AnimationPredictiveBackExitFloatSpec
|
||||
} else if (active) {
|
||||
AnimationEnterFloatSpec
|
||||
} else {
|
||||
AnimationExitFloatSpec
|
||||
}
|
||||
animationProgress.animateTo(
|
||||
targetValue = if (active) 1f else 0f,
|
||||
animationSpec = animationSpec,
|
||||
)
|
||||
}
|
||||
val mutatorMutex = remember { MutatorMutex() }
|
||||
if (active) {
|
||||
PredictiveBackHandler(true) { progress ->
|
||||
mutatorMutex.mutate {
|
||||
var canceled = false
|
||||
try {
|
||||
progress.collect { backEvent ->
|
||||
if (firstBackEvent == null) {
|
||||
firstBackEvent = backEvent
|
||||
}
|
||||
currentBackEvent = backEvent
|
||||
val interpolatedProgress = EaseOut.transform(backEvent.progress)
|
||||
animationProgress.snapTo(targetValue = 1 - interpolatedProgress)
|
||||
}
|
||||
} catch (e: CancellationException) {
|
||||
canceled = true
|
||||
} finally {
|
||||
firstBackEvent = null
|
||||
currentBackEvent = null
|
||||
onActiveChange(canceled)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun calculatePredictiveBackOffsetX(
|
||||
constraints: Constraints,
|
||||
minMargin: Int,
|
||||
currentBackEvent: BackEventCompat?,
|
||||
progress: Float,
|
||||
): Int {
|
||||
if (currentBackEvent == null) {
|
||||
return 0
|
||||
}
|
||||
val directionMultiplier = if (currentBackEvent.swipeEdge == BackEventCompat.EDGE_LEFT) 1 else -1
|
||||
val maxOffsetX =
|
||||
(constraints.maxWidth * SearchBarPredictiveBackMaxOffsetXRatio) - minMargin
|
||||
return (maxOffsetX * (1 - progress)).toInt() * directionMultiplier
|
||||
}
|
||||
|
||||
private fun calculatePredictiveBackOffsetY(
|
||||
constraints: Constraints,
|
||||
minMargin: Int,
|
||||
currentBackEvent: BackEventCompat?,
|
||||
firstBackEvent: BackEventCompat?,
|
||||
height: Int,
|
||||
maxOffsetY: Int,
|
||||
): Int {
|
||||
if (firstBackEvent == null || currentBackEvent == null) {
|
||||
return 0
|
||||
}
|
||||
val availableVerticalSpace = max(0, (constraints.maxHeight - height) / 2 - minMargin)
|
||||
val adjustedMaxOffsetY = min(availableVerticalSpace, maxOffsetY)
|
||||
val yDelta = currentBackEvent.touchY - firstBackEvent.touchY
|
||||
val yProgress = abs(yDelta) / constraints.maxHeight
|
||||
val directionMultiplier = sign(yDelta)
|
||||
return (lerp(0, adjustedMaxOffsetY, yProgress) * directionMultiplier).roundToInt()
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SearchBarInputField(
|
||||
state: TextFieldState,
|
||||
onSearch: () -> Unit,
|
||||
onActiveChange: (Boolean) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
label: @Composable (() -> Unit)? = null,
|
||||
placeholder: @Composable (() -> Unit)? = null,
|
||||
leadingIcon: @Composable (() -> Unit)? = null,
|
||||
trailingIcon: @Composable (() -> Unit)? = null,
|
||||
colors: TextFieldColors = SearchBarDefaults.inputFieldColors(),
|
||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||
) {
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
val focused by interactionSource.collectIsFocusedAsState()
|
||||
val textColor = LocalTextStyle.current.color.takeOrElse {
|
||||
if (focused) colors.focusedTextColor else colors.unfocusedTextColor
|
||||
}
|
||||
|
||||
BasicTextField2(
|
||||
state = state,
|
||||
modifier = modifier
|
||||
.height(InputFieldHeight)
|
||||
.fillMaxWidth()
|
||||
.focusRequester(focusRequester)
|
||||
.onFocusChanged { if (it.isFocused) onActiveChange(true) }
|
||||
.semantics {
|
||||
onClick {
|
||||
focusRequester.requestFocus()
|
||||
true
|
||||
}
|
||||
},
|
||||
enabled = enabled,
|
||||
lineLimits = TextFieldLineLimits.SingleLine,
|
||||
textStyle = LocalTextStyle.current.merge(TextStyle(color = textColor)),
|
||||
cursorBrush = SolidColor(colors.cursorColor),
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
|
||||
keyboardActions = KeyboardActions(onSearch = { onSearch() }),
|
||||
interactionSource = interactionSource,
|
||||
decorator = @Composable { innerTextField ->
|
||||
TextFieldDefaults.DecorationBox(
|
||||
value = state.text.toString(),
|
||||
innerTextField = innerTextField,
|
||||
enabled = enabled,
|
||||
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 = {},
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Stable
|
||||
private class AnimatedPaddingValues2(
|
||||
val animationProgress: State<Float>,
|
||||
val topPadding: State<Dp>,
|
||||
) : PaddingValues {
|
||||
override fun calculateTopPadding(): Dp = topPadding.value * animationProgress.value
|
||||
override fun calculateBottomPadding(): Dp = SearchBarVerticalPadding * animationProgress.value
|
||||
|
||||
override fun calculateLeftPadding(layoutDirection: LayoutDirection): Dp = 0.dp
|
||||
override fun calculateRightPadding(layoutDirection: LayoutDirection): Dp = 0.dp
|
||||
}
|
||||
|
||||
// Measurement specs
|
||||
private val SearchBarCornerRadius: Dp = InputFieldHeight / 2
|
||||
internal val SearchBarMinWidth: Dp = 360.dp
|
||||
private val SearchBarMaxWidth: Dp = 720.dp
|
||||
internal val SearchBarVerticalPadding: Dp = 8.dp
|
||||
internal val SearchBarHorizontalPadding: Dp = 16.dp
|
||||
|
||||
// Search bar has 16dp padding between icons and start/end, while by default text field has 12dp.
|
||||
private val SearchBarIconOffsetX: Dp = 4.dp
|
||||
|
||||
private const val SearchBarPredictiveBackMinScale: Float = 9f / 10f
|
||||
private val SearchBarPredictiveBackMinMargin: Dp = 8.dp
|
||||
private const val SearchBarPredictiveBackMaxOffsetXRatio: Float = 1f / 20f
|
||||
private val SearchBarPredictiveBackMaxOffsetY: Dp = 24.dp
|
||||
|
||||
// Animation specs
|
||||
private const val AnimationEnterDurationMillis: Int = 600
|
||||
private const val AnimationExitDurationMillis: Int = 350
|
||||
private const val AnimationDelayMillis: Int = 100
|
||||
private val AnimationEnterEasing = CubicBezierEasing(0.05f, 0.7f, 0.1f, 1.0f)
|
||||
private val AnimationExitEasing = CubicBezierEasing(0.0f, 1.0f, 0.0f, 1.0f)
|
||||
private val AnimationEnterFloatSpec: FiniteAnimationSpec<Float> = tween(
|
||||
durationMillis = AnimationEnterDurationMillis,
|
||||
delayMillis = AnimationDelayMillis,
|
||||
easing = AnimationEnterEasing,
|
||||
)
|
||||
private val AnimationExitFloatSpec: FiniteAnimationSpec<Float> = tween(
|
||||
durationMillis = AnimationExitDurationMillis,
|
||||
delayMillis = AnimationDelayMillis,
|
||||
easing = AnimationExitEasing,
|
||||
)
|
||||
private val AnimationPredictiveBackExitFloatSpec: FiniteAnimationSpec<Float> = tween(
|
||||
durationMillis = AnimationExitDurationMillis,
|
||||
easing = AnimationExitEasing,
|
||||
)
|
||||
+40
-212
@@ -1,4 +1,4 @@
|
||||
@file:Suppress("ktlint:standard:property-naming", "ktlint:standard:function-naming")
|
||||
@file:Suppress("ktlint:standard:property-naming")
|
||||
|
||||
package androidx.compose.material3
|
||||
|
||||
@@ -7,13 +7,11 @@ import androidx.compose.animation.core.TweenSpec
|
||||
import androidx.compose.animation.core.animate
|
||||
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
|
||||
@@ -25,6 +23,7 @@ import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
@@ -40,19 +39,13 @@ 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.unit.lerp
|
||||
import androidx.compose.ui.util.lerp
|
||||
import com.hippo.ehviewer.ui.tools.animateFloatMergeOneWayPredictiveBackAsState
|
||||
import kotlin.math.roundToInt
|
||||
@@ -60,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
|
||||
@@ -73,10 +63,7 @@ class DrawerState2(
|
||||
|
||||
val anchoredDraggableState = AnchoredDraggableState(
|
||||
initialValue = initialValue,
|
||||
animationSpec = AnimationSpec,
|
||||
confirmValueChange = confirmStateChange,
|
||||
positionalThreshold = { distance -> distance * DrawerPositionalThreshold },
|
||||
velocityThreshold = { with(requireDensity()) { DrawerVelocityThreshold.toPx() } },
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -234,79 +221,52 @@ class DrawerState2(
|
||||
* @param confirmStateChange Optional callback invoked to confirm or veto a pending state change.
|
||||
*/
|
||||
@Composable
|
||||
fun rememberDrawerState(
|
||||
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
|
||||
fun ModalNavigationDrawer(
|
||||
fun ModalSideDrawer(
|
||||
drawerContent: @Composable () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
drawerState: DrawerState2 = rememberDrawerState(DrawerValue.Closed),
|
||||
drawerState: DrawerState2 = rememberDrawerState2(DrawerValue.Closed),
|
||||
gesturesEnabled: Boolean = true,
|
||||
scrimColor: Color = DrawerDefaults.scrimColor,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val density = LocalDensity.current
|
||||
var minValue by remember { mutableStateOf(-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.isClosed) || (slop < 0 && drawerState.isOpen)
|
||||
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)
|
||||
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)
|
||||
@@ -328,145 +288,7 @@ fun ModalNavigationDrawer(
|
||||
}
|
||||
},
|
||||
fraction = {
|
||||
calculateFraction(minValue, maxValue, drawerState.requireOffset())
|
||||
},
|
||||
color = scrimColor,
|
||||
)
|
||||
val predictiveState by animateFloatMergeOneWayPredictiveBackAsState(drawerState.isOpen) {
|
||||
drawerState.close()
|
||||
}
|
||||
val absoluteElevation = LocalAbsoluteTonalElevation.current + DrawerDefaults.ModalDrawerElevation
|
||||
val predictiveModifier = if (drawerState.isOpen) {
|
||||
if (predictiveState > 0) {
|
||||
Modifier.layout { measurable, constraints ->
|
||||
val placeable = measurable.measure(constraints)
|
||||
val multiplierX = lerp(1f, 1.05f, predictiveState)
|
||||
val multiplierY = lerp(1f, 0.95f, predictiveState)
|
||||
val scaledWidth = (multiplierX * placeable.width).roundToInt()
|
||||
val scaledHeight = (multiplierY * placeable.height).roundToInt()
|
||||
val reMeasured = measurable.measure(Constraints.fixed(scaledWidth, scaledHeight))
|
||||
layout(scaledWidth, scaledHeight) {
|
||||
reMeasured.placeRelative(scaledWidth - placeable.width, 0)
|
||||
}
|
||||
}.background(
|
||||
color = MaterialTheme.colorScheme.surfaceColorAtElevation(absoluteElevation),
|
||||
shape = DrawerDefaults.shape,
|
||||
)
|
||||
} else {
|
||||
Modifier.graphicsLayer {
|
||||
val scale = lerp(1f, 0.95f, -predictiveState)
|
||||
scaleX = scale
|
||||
scaleY = scale
|
||||
}.offset {
|
||||
IntOffset(lerp(0f, minValue * 0.05f, -predictiveState).roundToInt(), 0)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Modifier.onGloballyPositioned {
|
||||
val w = it.size.width
|
||||
if (w != 0) {
|
||||
minValue = -it.size.width.toFloat()
|
||||
}
|
||||
}
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier.offset {
|
||||
IntOffset(drawerState.requireOffset().roundToInt(), 0)
|
||||
} then predictiveModifier.align(Alignment.CenterStart),
|
||||
contentAlignment = Alignment.CenterEnd,
|
||||
) {
|
||||
drawerContent()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SideDrawer(
|
||||
drawerContent: @Composable () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
drawerState: DrawerState2 = rememberDrawerState(DrawerValue.Closed),
|
||||
gesturesEnabled: Boolean = true,
|
||||
scrimColor: Color = DrawerDefaults.scrimColor,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val density = LocalDensity.current
|
||||
var minValue by remember { mutableStateOf(with(density) { ContainerWidth.toPx() }) }
|
||||
val maxValue = 0f
|
||||
|
||||
SideEffect {
|
||||
drawerState.density = density
|
||||
drawerState.anchoredDraggableState.updateAnchors(
|
||||
DraggableAnchors {
|
||||
DrawerValue.Closed at minValue
|
||||
DrawerValue.Open at maxValue
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
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)) {
|
||||
val radius by remember {
|
||||
snapshotFlow {
|
||||
val step = calculateFraction(minValue, maxValue, drawerState.currentOffset)
|
||||
lerp(0, 10, step).dp
|
||||
}
|
||||
}.collectAsState(0.dp)
|
||||
val blurModifier = Modifier.graphicsLayer {
|
||||
if (radius != 0.dp) {
|
||||
renderEffect = BlurEffect(radius.toPx(), radius.toPx(), TileMode.Clamp)
|
||||
shape = RectangleShape
|
||||
clip = true
|
||||
}
|
||||
}
|
||||
Box(modifier = blurModifier) {
|
||||
content()
|
||||
}
|
||||
Scrim(
|
||||
open = drawerState.isOpen,
|
||||
onClose = {
|
||||
if (gesturesEnabled && drawerState.confirmStateChange(DrawerValue.Closed)) {
|
||||
scope.launch { drawerState.close() }
|
||||
}
|
||||
},
|
||||
fraction = {
|
||||
calculateFraction(minValue, maxValue, drawerState.requireOffset())
|
||||
calculateFraction(maxValue, minValue, drawerState.requireOffset())
|
||||
},
|
||||
color = scrimColor,
|
||||
)
|
||||
@@ -484,7 +306,7 @@ fun SideDrawer(
|
||||
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),
|
||||
@@ -496,14 +318,20 @@ fun SideDrawer(
|
||||
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
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
/*
|
||||
* 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.navigation.compose
|
||||
|
||||
import androidx.compose.animation.AnimatedContentTransitionScope
|
||||
import androidx.compose.animation.EnterTransition
|
||||
import androidx.compose.animation.ExitTransition
|
||||
import androidx.navigation.NavBackStackEntry
|
||||
import androidx.navigation.NavGraph
|
||||
import androidx.navigation.NavGraphNavigator
|
||||
import androidx.navigation.Navigator
|
||||
import androidx.navigation.NavigatorProvider
|
||||
|
||||
/**
|
||||
* Custom subclass of [NavGraphNavigator] that adds support for defining
|
||||
* transitions at the navigation graph level.
|
||||
*/
|
||||
@Navigator.Name("navigation")
|
||||
internal class ComposeNavGraphNavigator(
|
||||
navigatorProvider: NavigatorProvider,
|
||||
) : NavGraphNavigator(navigatorProvider) {
|
||||
override fun createDestination(): NavGraph {
|
||||
return ComposeNavGraph(this)
|
||||
}
|
||||
|
||||
internal class ComposeNavGraph(
|
||||
navGraphNavigator: Navigator<out NavGraph>,
|
||||
) : NavGraph(navGraphNavigator) {
|
||||
internal var enterTransition: (
|
||||
@JvmSuppressWildcards
|
||||
AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition?
|
||||
)? = null
|
||||
|
||||
internal var exitTransition: (
|
||||
@JvmSuppressWildcards
|
||||
AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition?
|
||||
)? = null
|
||||
|
||||
internal var popEnterTransition: (
|
||||
@JvmSuppressWildcards
|
||||
AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition?
|
||||
)? = null
|
||||
|
||||
internal var popExitTransition: (
|
||||
@JvmSuppressWildcards
|
||||
AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition?
|
||||
)? = null
|
||||
}
|
||||
}
|
||||
@@ -1,128 +0,0 @@
|
||||
/*
|
||||
* 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.navigation.compose
|
||||
|
||||
import androidx.compose.animation.AnimatedContentScope
|
||||
import androidx.compose.animation.AnimatedContentTransitionScope
|
||||
import androidx.compose.animation.EnterTransition
|
||||
import androidx.compose.animation.ExitTransition
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.navigation.NavBackStackEntry
|
||||
import androidx.navigation.NavDestination
|
||||
import androidx.navigation.NavOptions
|
||||
import androidx.navigation.Navigator
|
||||
import androidx.navigation.compose.ComposeNavigator.Destination
|
||||
|
||||
/**
|
||||
* Navigator that navigates through [Composable]s. Every destination using this Navigator must
|
||||
* set a valid [Composable] by setting it directly on an instantiated [Destination] or calling
|
||||
* [composable].
|
||||
*/
|
||||
@Navigator.Name("composable")
|
||||
public class ComposeNavigator : Navigator<Destination>() {
|
||||
|
||||
/**
|
||||
* Get the map of transitions currently in progress from the [state].
|
||||
*/
|
||||
internal val transitionsInProgress get() = state.transitionsInProgress
|
||||
|
||||
/**
|
||||
* Get the back stack from the [state].
|
||||
*/
|
||||
public val backStack get() = state.backStack
|
||||
|
||||
internal val isPop = mutableStateOf(false)
|
||||
|
||||
override fun navigate(
|
||||
entries: List<NavBackStackEntry>,
|
||||
navOptions: NavOptions?,
|
||||
navigatorExtras: Extras?,
|
||||
) {
|
||||
entries.forEach { entry ->
|
||||
state.pushWithTransition(entry)
|
||||
}
|
||||
isPop.value = false
|
||||
}
|
||||
|
||||
override fun createDestination(): Destination {
|
||||
return Destination(this) { }
|
||||
}
|
||||
|
||||
override fun popBackStack(popUpTo: NavBackStackEntry, savedState: Boolean) {
|
||||
state.popWithTransition(popUpTo, savedState)
|
||||
isPop.value = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback to mark a navigation in transition as complete.
|
||||
*
|
||||
* This should be called in conjunction with [navigate] and [popBackStack] as those
|
||||
* calls merely start a transition to the target destination, and requires manually marking
|
||||
* the transition as complete by calling this method.
|
||||
*
|
||||
* Failing to call this method could result in entries being prevented from reaching their
|
||||
* final [Lifecycle.State].
|
||||
*/
|
||||
public fun onTransitionComplete(entry: NavBackStackEntry) {
|
||||
state.markTransitionComplete(entry)
|
||||
}
|
||||
|
||||
/**
|
||||
* NavDestination specific to [ComposeNavigator]
|
||||
*/
|
||||
@NavDestination.ClassType(Composable::class)
|
||||
public class Destination(
|
||||
navigator: ComposeNavigator,
|
||||
internal val content:
|
||||
@Composable AnimatedContentScope.(@JvmSuppressWildcards NavBackStackEntry) -> Unit,
|
||||
) : NavDestination(navigator) {
|
||||
|
||||
@Deprecated(
|
||||
message = "Deprecated in favor of Destination that supports AnimatedContent",
|
||||
level = DeprecationLevel.HIDDEN,
|
||||
)
|
||||
constructor(
|
||||
navigator: ComposeNavigator,
|
||||
content: @Composable (NavBackStackEntry) -> @JvmSuppressWildcards Unit,
|
||||
) : this(navigator, content = { entry -> content(entry) })
|
||||
|
||||
internal var enterTransition: (
|
||||
@JvmSuppressWildcards
|
||||
AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition?
|
||||
)? = null
|
||||
|
||||
internal var exitTransition: (
|
||||
@JvmSuppressWildcards
|
||||
AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition?
|
||||
)? = null
|
||||
|
||||
internal var popEnterTransition: (
|
||||
@JvmSuppressWildcards
|
||||
AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition?
|
||||
)? = null
|
||||
|
||||
internal var popExitTransition: (
|
||||
@JvmSuppressWildcards
|
||||
AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition?
|
||||
)? = null
|
||||
}
|
||||
|
||||
internal companion object {
|
||||
internal const val NAME = "composable"
|
||||
}
|
||||
}
|
||||
@@ -1,137 +0,0 @@
|
||||
/*
|
||||
* Copyright 2021 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.navigation.compose
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveableStateHolder
|
||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||
import androidx.compose.ui.platform.LocalInspectionMode
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
import androidx.navigation.NavBackStackEntry
|
||||
import androidx.navigation.compose.DialogNavigator.Destination
|
||||
|
||||
/**
|
||||
* Show each [Destination] on the [DialogNavigator]'s back stack as a [Dialog].
|
||||
*
|
||||
* Note that [NavHost] will call this for you; you do not need to call it manually.
|
||||
*/
|
||||
@Composable
|
||||
public fun DialogHost(dialogNavigator: DialogNavigator) {
|
||||
val saveableStateHolder = rememberSaveableStateHolder()
|
||||
val dialogBackStack by dialogNavigator.backStack.collectAsState()
|
||||
val visibleBackStack = rememberVisibleList(dialogBackStack)
|
||||
visibleBackStack.PopulateVisibleList(dialogBackStack)
|
||||
|
||||
val transitionInProgress by dialogNavigator.transitionInProgress.collectAsState()
|
||||
val dialogsToDispose = remember { mutableStateListOf<NavBackStackEntry>() }
|
||||
|
||||
visibleBackStack.forEach { backStackEntry ->
|
||||
val destination = backStackEntry.destination as Destination
|
||||
Dialog(
|
||||
onDismissRequest = { dialogNavigator.dismiss(backStackEntry) },
|
||||
properties = destination.dialogProperties,
|
||||
) {
|
||||
DisposableEffect(backStackEntry) {
|
||||
dialogsToDispose.add(backStackEntry)
|
||||
onDispose {
|
||||
dialogNavigator.onTransitionComplete(backStackEntry)
|
||||
dialogsToDispose.remove(backStackEntry)
|
||||
}
|
||||
}
|
||||
|
||||
// while in the scope of the composable, we provide the navBackStackEntry as the
|
||||
// ViewModelStoreOwner and LifecycleOwner
|
||||
backStackEntry.LocalOwnersProvider(saveableStateHolder) {
|
||||
destination.content(backStackEntry)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Dialogs may have been popped before it was composed. To prevent leakage, we need to
|
||||
// mark popped entries as complete here. Check that we don't accidentally complete popped
|
||||
// entries that were composed, unless they were disposed of already.
|
||||
LaunchedEffect(transitionInProgress, dialogsToDispose) {
|
||||
transitionInProgress.forEach { entry ->
|
||||
if (!dialogNavigator.backStack.value.contains(entry) &&
|
||||
!dialogsToDispose.contains(entry)
|
||||
) {
|
||||
dialogNavigator.onTransitionComplete(entry)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun MutableList<NavBackStackEntry>.PopulateVisibleList(
|
||||
backStack: Collection<NavBackStackEntry>,
|
||||
) {
|
||||
val isInspecting = LocalInspectionMode.current
|
||||
backStack.forEach { entry ->
|
||||
DisposableEffect(entry.lifecycle) {
|
||||
val observer = LifecycleEventObserver { _, event ->
|
||||
// show dialog in preview
|
||||
if (isInspecting && !contains(entry)) {
|
||||
add(entry)
|
||||
}
|
||||
// ON_START -> add to visibleBackStack, ON_STOP -> remove from visibleBackStack
|
||||
if (event == Lifecycle.Event.ON_START) {
|
||||
// We want to treat the visible lists as Sets but we want to keep
|
||||
// the functionality of mutableStateListOf() so that we recompose in response
|
||||
// to adds and removes.
|
||||
if (!contains(entry)) {
|
||||
add(entry)
|
||||
}
|
||||
}
|
||||
if (event == Lifecycle.Event.ON_STOP) {
|
||||
remove(entry)
|
||||
}
|
||||
}
|
||||
entry.lifecycle.addObserver(observer)
|
||||
onDispose {
|
||||
entry.lifecycle.removeObserver(observer)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun rememberVisibleList(
|
||||
backStack: Collection<NavBackStackEntry>,
|
||||
): SnapshotStateList<NavBackStackEntry> {
|
||||
// show dialog in preview
|
||||
val isInspecting = LocalInspectionMode.current
|
||||
return remember(backStack) {
|
||||
mutableStateListOf<NavBackStackEntry>().also {
|
||||
it.addAll(
|
||||
backStack.filter { entry ->
|
||||
if (isInspecting) {
|
||||
true
|
||||
} else {
|
||||
entry.lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
/*
|
||||
* 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.navigation.compose
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.navigation.FloatingWindow
|
||||
import androidx.navigation.NavBackStackEntry
|
||||
import androidx.navigation.NavDestination
|
||||
import androidx.navigation.NavOptions
|
||||
import androidx.navigation.Navigator
|
||||
import androidx.navigation.compose.DialogNavigator.Destination
|
||||
|
||||
/**
|
||||
* Navigator that navigates through [Composable]s that will be hosted within a
|
||||
* [Dialog]. Every destination using this Navigator must set a valid [Composable] by setting it
|
||||
* directly on an instantiated [Destination] or calling [dialog].
|
||||
*/
|
||||
@Navigator.Name("dialog")
|
||||
public class DialogNavigator : Navigator<Destination>() {
|
||||
|
||||
/**
|
||||
* Get the back stack from the [state].
|
||||
*/
|
||||
internal val backStack get() = state.backStack
|
||||
|
||||
/**
|
||||
* Get the transitioning dialogs from the [state].
|
||||
*/
|
||||
internal val transitionInProgress get() = state.transitionsInProgress
|
||||
|
||||
/**
|
||||
* Dismiss the dialog destination associated with the given [backStackEntry].
|
||||
*/
|
||||
internal fun dismiss(backStackEntry: NavBackStackEntry) {
|
||||
popBackStack(backStackEntry, false)
|
||||
}
|
||||
|
||||
override fun navigate(
|
||||
entries: List<NavBackStackEntry>,
|
||||
navOptions: NavOptions?,
|
||||
navigatorExtras: Extras?,
|
||||
) {
|
||||
entries.forEach { entry ->
|
||||
state.push(entry)
|
||||
}
|
||||
}
|
||||
|
||||
override fun createDestination(): Destination {
|
||||
return Destination(this) { }
|
||||
}
|
||||
|
||||
override fun popBackStack(popUpTo: NavBackStackEntry, savedState: Boolean) {
|
||||
state.popWithTransition(popUpTo, savedState)
|
||||
// When popping, the incoming dialog is marked transitioning to hold it in
|
||||
// STARTED. With pop complete, we can remove it from transition so it can move to RESUMED.
|
||||
val popIndex = state.transitionsInProgress.value.indexOf(popUpTo)
|
||||
// do not mark complete for entries up to and including popUpTo
|
||||
state.transitionsInProgress.value.forEachIndexed { index, entry ->
|
||||
if (index > popIndex) onTransitionComplete(entry)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun onTransitionComplete(entry: NavBackStackEntry) {
|
||||
state.markTransitionComplete(entry)
|
||||
}
|
||||
|
||||
/**
|
||||
* NavDestination specific to [DialogNavigator]
|
||||
*/
|
||||
@NavDestination.ClassType(Composable::class)
|
||||
public class Destination(
|
||||
navigator: DialogNavigator,
|
||||
internal val dialogProperties: DialogProperties = DialogProperties(),
|
||||
internal val content: @Composable (NavBackStackEntry) -> Unit,
|
||||
) : NavDestination(navigator), FloatingWindow
|
||||
|
||||
internal companion object {
|
||||
internal const val NAME = "dialog"
|
||||
}
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
/*
|
||||
* Copyright 2021 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.
|
||||
*/
|
||||
|
||||
@file:Suppress("ktlint:standard:property-naming", "ktlint:standard:function-naming")
|
||||
|
||||
package androidx.navigation.compose
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.saveable.SaveableStateHolder
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.compose.ui.platform.LocalSavedStateRegistryOwner
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import androidx.navigation.NavBackStackEntry
|
||||
import java.lang.ref.WeakReference
|
||||
import java.util.UUID
|
||||
|
||||
/**
|
||||
* Provides [this] [NavBackStackEntry] as [LocalViewModelStoreOwner], [LocalLifecycleOwner] and
|
||||
* [LocalSavedStateRegistryOwner] to the [content] and saves the [content]'s saveable states with
|
||||
* the given [saveableStateHolder].
|
||||
*
|
||||
* @param saveableStateHolder The [SaveableStateHolder] that holds the saved states. The same
|
||||
* holder should be used for all [NavBackStackEntry]s in the encapsulating [Composable] and the
|
||||
* holder should be hoisted.
|
||||
* @param content The content [Composable]
|
||||
*/
|
||||
@Composable
|
||||
public fun NavBackStackEntry.LocalOwnersProvider(
|
||||
saveableStateHolder: SaveableStateHolder,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
CompositionLocalProvider(
|
||||
LocalViewModelStoreOwner provides this,
|
||||
LocalLifecycleOwner provides this,
|
||||
LocalSavedStateRegistryOwner provides this,
|
||||
) {
|
||||
saveableStateHolder.SaveableStateProvider(content)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SaveableStateHolder.SaveableStateProvider(content: @Composable () -> Unit) {
|
||||
val viewModel = viewModel<BackStackEntryIdViewModel>()
|
||||
// Stash a reference to the SaveableStateHolder in the ViewModel so that
|
||||
// it is available when the ViewModel is cleared, marking the permanent removal of this
|
||||
// NavBackStackEntry from the back stack. Which, because of animations,
|
||||
// only happens after this leaves composition. Which means we can't rely on
|
||||
// DisposableEffect to clean up this reference (as it'll be cleaned up too early)
|
||||
viewModel.saveableStateHolderRef = WeakReference(this)
|
||||
SaveableStateProvider(viewModel.id, content)
|
||||
}
|
||||
|
||||
internal class BackStackEntryIdViewModel(handle: SavedStateHandle) : ViewModel() {
|
||||
|
||||
private val IdKey = "SaveableStateHolder_BackStackEntryKey"
|
||||
|
||||
// we create our own id for each back stack entry to support multiple entries of the same
|
||||
// destination. this id will be restored by SavedStateHandle
|
||||
val id: UUID = handle.get<UUID>(IdKey) ?: UUID.randomUUID().also { handle.set(IdKey, it) }
|
||||
|
||||
lateinit var saveableStateHolderRef: WeakReference<SaveableStateHolder>
|
||||
|
||||
// onCleared will be called on the entries removed from the back stack. here we notify
|
||||
// SaveableStateProvider that we should remove any state is had associated with this
|
||||
// destination as it is no longer needed.
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
saveableStateHolderRef.get()?.removeState(id)
|
||||
saveableStateHolderRef.clear()
|
||||
}
|
||||
}
|
||||
@@ -1,243 +0,0 @@
|
||||
/*
|
||||
* 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.navigation.compose
|
||||
|
||||
import androidx.compose.animation.AnimatedContentScope
|
||||
import androidx.compose.animation.AnimatedContentTransitionScope
|
||||
import androidx.compose.animation.EnterTransition
|
||||
import androidx.compose.animation.ExitTransition
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.navigation.NamedNavArgument
|
||||
import androidx.navigation.NavBackStackEntry
|
||||
import androidx.navigation.NavDeepLink
|
||||
import androidx.navigation.NavGraph
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.get
|
||||
|
||||
/**
|
||||
* Add the [Composable] to the [NavGraphBuilder]
|
||||
*
|
||||
* @param route route for the destination
|
||||
* @param arguments list of arguments to associate with destination
|
||||
* @param deepLinks list of deep links to associate with the destinations
|
||||
* @param content composable for the destination
|
||||
*/
|
||||
@Deprecated(
|
||||
message = "Deprecated in favor of composable builder that supports AnimatedContent",
|
||||
level = DeprecationLevel.HIDDEN,
|
||||
)
|
||||
public fun NavGraphBuilder.composable(
|
||||
route: String,
|
||||
arguments: List<NamedNavArgument> = emptyList(),
|
||||
deepLinks: List<NavDeepLink> = emptyList(),
|
||||
content: @Composable (NavBackStackEntry) -> Unit,
|
||||
) {
|
||||
addDestination(
|
||||
ComposeNavigator.Destination(provider[ComposeNavigator::class]) {
|
||||
entry ->
|
||||
content(entry)
|
||||
}.apply {
|
||||
this.route = route
|
||||
arguments.forEach { (argumentName, argument) ->
|
||||
addArgument(argumentName, argument)
|
||||
}
|
||||
deepLinks.forEach { deepLink ->
|
||||
addDeepLink(deepLink)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the [Composable] to the [NavGraphBuilder]
|
||||
*
|
||||
* @param route route for the destination
|
||||
* @param arguments list of arguments to associate with destination
|
||||
* @param deepLinks list of deep links to associate with the destinations
|
||||
* @param enterTransition callback to determine the destination's enter transition
|
||||
* @param exitTransition callback to determine the destination's exit transition
|
||||
* @param popEnterTransition callback to determine the destination's popEnter transition
|
||||
* @param popExitTransition callback to determine the destination's popExit transition
|
||||
* @param content composable for the destination
|
||||
*/
|
||||
public fun NavGraphBuilder.composable(
|
||||
route: String,
|
||||
arguments: List<NamedNavArgument> = emptyList(),
|
||||
deepLinks: List<NavDeepLink> = emptyList(),
|
||||
enterTransition: (
|
||||
@JvmSuppressWildcards
|
||||
AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition?
|
||||
)? = null,
|
||||
exitTransition: (
|
||||
@JvmSuppressWildcards
|
||||
AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition?
|
||||
)? = null,
|
||||
popEnterTransition: (
|
||||
@JvmSuppressWildcards
|
||||
AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition?
|
||||
)? =
|
||||
enterTransition,
|
||||
popExitTransition: (
|
||||
@JvmSuppressWildcards
|
||||
AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition?
|
||||
)? =
|
||||
exitTransition,
|
||||
content: @Composable AnimatedContentScope.(NavBackStackEntry) -> Unit,
|
||||
) {
|
||||
addDestination(
|
||||
ComposeNavigator.Destination(
|
||||
provider[ComposeNavigator::class],
|
||||
content,
|
||||
).apply {
|
||||
this.route = route
|
||||
arguments.forEach { (argumentName, argument) ->
|
||||
addArgument(argumentName, argument)
|
||||
}
|
||||
deepLinks.forEach { deepLink ->
|
||||
addDeepLink(deepLink)
|
||||
}
|
||||
this.enterTransition = enterTransition
|
||||
this.exitTransition = exitTransition
|
||||
this.popEnterTransition = popEnterTransition
|
||||
this.popExitTransition = popExitTransition
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a nested [NavGraph]
|
||||
*
|
||||
* @sample androidx.navigation.compose.samples.NestedNavInGraphWithArgs
|
||||
*
|
||||
* @param startDestination the starting destination's route for this NavGraph
|
||||
* @param route the destination's unique route
|
||||
* @param arguments list of arguments to associate with destination
|
||||
* @param deepLinks list of deep links to associate with the destinations
|
||||
* @param builder the builder used to construct the graph
|
||||
*/
|
||||
@Deprecated(
|
||||
message = "Deprecated in favor of navigation builder that supports AnimatedContent",
|
||||
level = DeprecationLevel.HIDDEN,
|
||||
)
|
||||
public fun NavGraphBuilder.navigation(
|
||||
startDestination: String,
|
||||
route: String,
|
||||
arguments: List<NamedNavArgument> = emptyList(),
|
||||
deepLinks: List<NavDeepLink> = emptyList(),
|
||||
builder: NavGraphBuilder.() -> Unit,
|
||||
) {
|
||||
addDestination(
|
||||
NavGraphBuilder(provider, startDestination, route).apply(builder).build().apply {
|
||||
arguments.forEach { (argumentName, argument) ->
|
||||
addArgument(argumentName, argument)
|
||||
}
|
||||
deepLinks.forEach { deepLink ->
|
||||
addDeepLink(deepLink)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a nested [NavGraph]
|
||||
*
|
||||
* @param startDestination the starting destination's route for this NavGraph
|
||||
* @param route the destination's unique route
|
||||
* @param arguments list of arguments to associate with destination
|
||||
* @param deepLinks list of deep links to associate with the destinations
|
||||
* @param enterTransition callback to define enter transitions for destination in this NavGraph
|
||||
* @param exitTransition callback to define exit transitions for destination in this NavGraph
|
||||
* @param popEnterTransition callback to define pop enter transitions for destination in this
|
||||
* NavGraph
|
||||
* @param popExitTransition callback to define pop exit transitions for destination in this NavGraph
|
||||
* @param builder the builder used to construct the graph
|
||||
*
|
||||
* @return the newly constructed nested NavGraph
|
||||
*/
|
||||
public fun NavGraphBuilder.navigation(
|
||||
startDestination: String,
|
||||
route: String,
|
||||
arguments: List<NamedNavArgument> = emptyList(),
|
||||
deepLinks: List<NavDeepLink> = emptyList(),
|
||||
enterTransition: (AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition?)? =
|
||||
null,
|
||||
exitTransition: (AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition?)? =
|
||||
null,
|
||||
popEnterTransition: (
|
||||
AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition?
|
||||
)? = enterTransition,
|
||||
popExitTransition: (
|
||||
AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition?
|
||||
)? = exitTransition,
|
||||
builder: NavGraphBuilder.() -> Unit,
|
||||
) {
|
||||
addDestination(
|
||||
NavGraphBuilder(provider, startDestination, route).apply(builder).build().apply {
|
||||
arguments.forEach { (argumentName, argument) ->
|
||||
addArgument(argumentName, argument)
|
||||
}
|
||||
deepLinks.forEach { deepLink ->
|
||||
addDeepLink(deepLink)
|
||||
}
|
||||
if (this is ComposeNavGraphNavigator.ComposeNavGraph) {
|
||||
this.enterTransition = enterTransition
|
||||
this.exitTransition = exitTransition
|
||||
this.popEnterTransition = popEnterTransition
|
||||
this.popExitTransition = popExitTransition
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the [Composable] to the [NavGraphBuilder] that will be hosted within a
|
||||
* [androidx.compose.ui.window.Dialog]. This is suitable only when this dialog represents
|
||||
* a separate screen in your app that needs its own lifecycle and saved state, independent
|
||||
* of any other destination in your navigation graph. For use cases such as `AlertDialog`,
|
||||
* you should use those APIs directly in the [composable] destination that wants to show that
|
||||
* dialog.
|
||||
*
|
||||
* @param route route for the destination
|
||||
* @param arguments list of arguments to associate with destination
|
||||
* @param deepLinks list of deep links to associate with the destinations
|
||||
* @param dialogProperties properties that should be passed to [androidx.compose.ui.window.Dialog].
|
||||
* @param content composable content for the destination that will be hosted within the Dialog
|
||||
*/
|
||||
public fun NavGraphBuilder.dialog(
|
||||
route: String,
|
||||
arguments: List<NamedNavArgument> = emptyList(),
|
||||
deepLinks: List<NavDeepLink> = emptyList(),
|
||||
dialogProperties: DialogProperties = DialogProperties(),
|
||||
content: @Composable (NavBackStackEntry) -> Unit,
|
||||
) {
|
||||
addDestination(
|
||||
DialogNavigator.Destination(
|
||||
provider[DialogNavigator::class],
|
||||
dialogProperties,
|
||||
content,
|
||||
).apply {
|
||||
this.route = route
|
||||
arguments.forEach { (argumentName, argument) ->
|
||||
addArgument(argumentName, argument)
|
||||
}
|
||||
deepLinks.forEach { deepLink ->
|
||||
addDeepLink(deepLink)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -1,424 +0,0 @@
|
||||
/*
|
||||
* 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.navigation.compose
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.activity.compose.PredictiveBackHandler
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.AnimatedContentTransitionScope
|
||||
import androidx.compose.animation.ContentTransform
|
||||
import androidx.compose.animation.EnterTransition
|
||||
import androidx.compose.animation.ExitTransition
|
||||
import androidx.compose.animation.core.Animatable
|
||||
import androidx.compose.animation.core.EaseOut
|
||||
import androidx.compose.animation.core.LinearEasing
|
||||
import androidx.compose.animation.core.MutableTransitionState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.core.updateTransition
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.togetherWith
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveableStateHolder
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner
|
||||
import androidx.navigation.NavBackStackEntry
|
||||
import androidx.navigation.NavDestination
|
||||
import androidx.navigation.NavDestination.Companion.hierarchy
|
||||
import androidx.navigation.NavGraph
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.Navigator
|
||||
import androidx.navigation.createGraph
|
||||
import androidx.navigation.get
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.collectAsState
|
||||
import kotlin.math.roundToLong
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.NonCancellable
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
/**
|
||||
* Provides in place in the Compose hierarchy for self contained navigation to occur.
|
||||
*
|
||||
* Once this is called, any Composable within the given [NavGraphBuilder] can be navigated to from
|
||||
* the provided [navController].
|
||||
*
|
||||
* The builder passed into this method is [remember]ed. This means that for this NavHost, the
|
||||
* contents of the builder cannot be changed.
|
||||
*
|
||||
* @sample androidx.navigation.compose.samples.NavScaffold
|
||||
*
|
||||
* @param navController the navController for this host
|
||||
* @param startDestination the route for the start destination
|
||||
* @param modifier The modifier to be applied to the layout.
|
||||
* @param route the route for the graph
|
||||
* @param builder the builder used to construct the graph
|
||||
*/
|
||||
@Deprecated(
|
||||
message = "Deprecated in favor of NavHost that supports AnimatedContent",
|
||||
level = DeprecationLevel.HIDDEN,
|
||||
)
|
||||
@Composable
|
||||
public fun NavHost(
|
||||
navController: NavHostController,
|
||||
startDestination: String,
|
||||
modifier: Modifier = Modifier,
|
||||
route: String? = null,
|
||||
builder: NavGraphBuilder.() -> Unit,
|
||||
) {
|
||||
NavHost(
|
||||
navController,
|
||||
remember(route, startDestination, builder) {
|
||||
navController.createGraph(startDestination, route, builder)
|
||||
},
|
||||
modifier,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides in place in the Compose hierarchy for self contained navigation to occur.
|
||||
*
|
||||
* Once this is called, any Composable within the given [NavGraphBuilder] can be navigated to from
|
||||
* the provided [navController].
|
||||
*
|
||||
* The builder passed into this method is [remember]ed. This means that for this NavHost, the
|
||||
* contents of the builder cannot be changed.
|
||||
*
|
||||
* @param navController the navController for this host
|
||||
* @param startDestination the route for the start destination
|
||||
* @param modifier The modifier to be applied to the layout.
|
||||
* @param contentAlignment The [Alignment] of the [AnimatedContent]
|
||||
* @param route the route for the graph
|
||||
* @param enterTransition callback to define enter transitions for destination in this host
|
||||
* @param exitTransition callback to define exit transitions for destination in this host
|
||||
* @param popEnterTransition callback to define popEnter transitions for destination in this host
|
||||
* @param popExitTransition callback to define popExit transitions for destination in this host
|
||||
* @param builder the builder used to construct the graph
|
||||
*/
|
||||
@Composable
|
||||
public fun NavHost(
|
||||
navController: NavHostController,
|
||||
startDestination: String,
|
||||
modifier: Modifier = Modifier,
|
||||
contentAlignment: Alignment = Alignment.Center,
|
||||
route: String? = null,
|
||||
enterTransition: (AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition) =
|
||||
{ fadeIn(animationSpec = tween(700)) },
|
||||
exitTransition: (AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition) =
|
||||
{ fadeOut(animationSpec = tween(700)) },
|
||||
popEnterTransition: (AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition) =
|
||||
enterTransition,
|
||||
popExitTransition: (AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition) =
|
||||
exitTransition,
|
||||
builder: NavGraphBuilder.() -> Unit,
|
||||
) {
|
||||
NavHost(
|
||||
navController,
|
||||
remember(route, startDestination, builder) {
|
||||
navController.createGraph(startDestination, route, builder)
|
||||
},
|
||||
modifier,
|
||||
contentAlignment,
|
||||
enterTransition,
|
||||
exitTransition,
|
||||
popEnterTransition,
|
||||
popExitTransition,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides in place in the Compose hierarchy for self contained navigation to occur.
|
||||
*
|
||||
* Once this is called, any Composable within the given [NavGraphBuilder] can be navigated to from
|
||||
* the provided [navController].
|
||||
*
|
||||
* The graph passed into this method is [remember]ed. This means that for this NavHost, the graph
|
||||
* cannot be changed.
|
||||
*
|
||||
* @param navController the navController for this host
|
||||
* @param graph the graph for this host
|
||||
* @param modifier The modifier to be applied to the layout.
|
||||
*/
|
||||
@Deprecated(
|
||||
message = "Deprecated in favor of NavHost that supports AnimatedContent",
|
||||
level = DeprecationLevel.HIDDEN,
|
||||
)
|
||||
@Composable
|
||||
public fun NavHost(
|
||||
navController: NavHostController,
|
||||
graph: NavGraph,
|
||||
modifier: Modifier = Modifier,
|
||||
) = NavHost(navController, graph, modifier)
|
||||
|
||||
/**
|
||||
* Provides in place in the Compose hierarchy for self contained navigation to occur.
|
||||
*
|
||||
* Once this is called, any Composable within the given [NavGraphBuilder] can be navigated to from
|
||||
* the provided [navController].
|
||||
*
|
||||
* @param navController the navController for this host
|
||||
* @param graph the graph for this host
|
||||
* @param modifier The modifier to be applied to the layout.
|
||||
* @param contentAlignment The [Alignment] of the [AnimatedContent]
|
||||
* @param enterTransition callback to define enter transitions for destination in this host
|
||||
* @param exitTransition callback to define exit transitions for destination in this host
|
||||
* @param popEnterTransition callback to define popEnter transitions for destination in this host
|
||||
* @param popExitTransition callback to define popExit transitions for destination in this host
|
||||
*/
|
||||
@SuppressLint("StateFlowValueCalledInComposition")
|
||||
@Composable
|
||||
public fun NavHost(
|
||||
navController: NavHostController,
|
||||
graph: NavGraph,
|
||||
modifier: Modifier = Modifier,
|
||||
contentAlignment: Alignment = Alignment.Center,
|
||||
enterTransition: (AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition) =
|
||||
{ fadeIn(animationSpec = tween(700)) },
|
||||
exitTransition: (AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition) =
|
||||
{ fadeOut(animationSpec = tween(700)) },
|
||||
popEnterTransition: (AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition) =
|
||||
enterTransition,
|
||||
popExitTransition: (AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition) =
|
||||
exitTransition,
|
||||
) {
|
||||
val lifecycleOwner = LocalLifecycleOwner.current
|
||||
val viewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) {
|
||||
"NavHost requires a ViewModelStoreOwner to be provided via LocalViewModelStoreOwner"
|
||||
}
|
||||
|
||||
navController.setViewModelStore(viewModelStoreOwner.viewModelStore)
|
||||
|
||||
// Then set the graph
|
||||
navController.graph = graph
|
||||
|
||||
// Find the ComposeNavigator, returning early if it isn't found
|
||||
// (such as is the case when using TestNavHostController)
|
||||
val composeNavigator = navController.navigatorProvider.get<Navigator<out NavDestination>>(
|
||||
ComposeNavigator.NAME,
|
||||
) as? ComposeNavigator ?: return
|
||||
|
||||
val currentBackStack by composeNavigator.backStack.collectAsState()
|
||||
|
||||
DisposableEffect(lifecycleOwner) {
|
||||
// Setup the navController with proper owners
|
||||
navController.setLifecycleOwner(lifecycleOwner)
|
||||
onDispose { }
|
||||
}
|
||||
|
||||
val saveableStateHolder = rememberSaveableStateHolder()
|
||||
|
||||
val allVisibleEntries by navController.visibleEntries.collectAsState()
|
||||
|
||||
// Intercept back only when there's a destination to pop
|
||||
val visibleEntries by remember {
|
||||
derivedStateOf {
|
||||
allVisibleEntries.filter { entry ->
|
||||
entry.destination.navigatorName == ComposeNavigator.NAME
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val backStackEntry: NavBackStackEntry? = visibleEntries.lastOrNull()
|
||||
|
||||
val zIndices = remember { mutableMapOf<String, Float>() }
|
||||
|
||||
if (backStackEntry != null) {
|
||||
val finalEnter: AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition = {
|
||||
val targetDestination = targetState.destination as ComposeNavigator.Destination
|
||||
|
||||
if (composeNavigator.isPop.value) {
|
||||
targetDestination.hierarchy.firstNotNullOfOrNull { destination ->
|
||||
destination.createPopEnterTransition(this)
|
||||
} ?: popEnterTransition.invoke(this)
|
||||
} else {
|
||||
targetDestination.hierarchy.firstNotNullOfOrNull { destination ->
|
||||
destination.createEnterTransition(this)
|
||||
} ?: enterTransition.invoke(this)
|
||||
}
|
||||
}
|
||||
|
||||
val finalExit: AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition = {
|
||||
val initialDestination = initialState.destination as ComposeNavigator.Destination
|
||||
|
||||
if (composeNavigator.isPop.value) {
|
||||
initialDestination.hierarchy.firstNotNullOfOrNull { destination ->
|
||||
destination.createPopExitTransition(this)
|
||||
} ?: popExitTransition.invoke(this)
|
||||
} else {
|
||||
initialDestination.hierarchy.firstNotNullOfOrNull { destination ->
|
||||
destination.createExitTransition(this)
|
||||
} ?: exitTransition.invoke(this)
|
||||
}
|
||||
}
|
||||
|
||||
DisposableEffect(true) {
|
||||
onDispose {
|
||||
visibleEntries.forEach { entry ->
|
||||
composeNavigator.onTransitionComplete(entry)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val enablePredictiveBack by Settings.predictiveNavAnim.collectAsState()
|
||||
val transition = if (enablePredictiveBack) {
|
||||
val duration = 500000000L
|
||||
val transitionState = remember { MutableTransitionState(backStackEntry) }
|
||||
val transition = updateTransition(transitionState = transitionState, label = "entry")
|
||||
val animatedFraction = remember { Animatable(0f).also { it.updateBounds(lowerBound = 0f, upperBound = 1f) } }
|
||||
|
||||
fun seekToFraction(lState: NavBackStackEntry, rState: NavBackStackEntry) {
|
||||
val fraction = animatedFraction.value
|
||||
val playTimeNanos = (fraction * duration).roundToLong()
|
||||
transition.setPlaytimeAfterInitialAndTargetStateEstablished(lState, rState, playTimeNanos)
|
||||
}
|
||||
|
||||
LaunchedEffect(backStackEntry) {
|
||||
try {
|
||||
animatedFraction.animateTo(1f, animationSpec = tween(500, easing = LinearEasing)) {
|
||||
seekToFraction(transitionState.currentState, backStackEntry)
|
||||
}
|
||||
} finally {
|
||||
withContext(NonCancellable) {
|
||||
animatedFraction.snapTo(0f)
|
||||
transition.setPlaytimeAfterInitialAndTargetStateEstablished(
|
||||
backStackEntry,
|
||||
backStackEntry,
|
||||
0,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PredictiveBackHandler(currentBackStack.size > 1) { flow ->
|
||||
// Prev can never be null, i.e. BackHandler should not enabled when prev is null
|
||||
val prev = currentBackStack.last { it != backStackEntry }
|
||||
try {
|
||||
flow.collect {
|
||||
val interpolated = EaseOut.transform(it.progress)
|
||||
animatedFraction.snapTo(interpolated)
|
||||
seekToFraction(backStackEntry, prev)
|
||||
}
|
||||
navController.popBackStack()
|
||||
} catch (e: CancellationException) {
|
||||
withContext(NonCancellable) {
|
||||
animatedFraction.snapTo(0f)
|
||||
transition.setPlaytimeAfterInitialAndTargetStateEstablished(
|
||||
backStackEntry,
|
||||
backStackEntry,
|
||||
0,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
transition
|
||||
} else {
|
||||
BackHandler(currentBackStack.size > 1) {
|
||||
navController.popBackStack()
|
||||
}
|
||||
updateTransition(backStackEntry, label = "entry")
|
||||
}
|
||||
|
||||
transition.AnimatedContent(
|
||||
modifier,
|
||||
transitionSpec = {
|
||||
// If the initialState of the AnimatedContent is not in visibleEntries, we are in
|
||||
// a case where visible has cleared the old state for some reason, so instead of
|
||||
// attempting to animate away from the initialState, we skip the animation.
|
||||
if (initialState in visibleEntries) {
|
||||
val initialZIndex = zIndices[initialState.id]
|
||||
?: 0f.also { zIndices[initialState.id] = 0f }
|
||||
val targetZIndex = when {
|
||||
targetState.id == initialState.id -> initialZIndex
|
||||
composeNavigator.isPop.value -> initialZIndex - 1f
|
||||
else -> initialZIndex + 1f
|
||||
}.also { zIndices[targetState.id] = it }
|
||||
|
||||
ContentTransform(finalEnter(this), finalExit(this), targetZIndex)
|
||||
} else {
|
||||
EnterTransition.None togetherWith ExitTransition.None
|
||||
}
|
||||
},
|
||||
contentAlignment,
|
||||
contentKey = { it.id },
|
||||
) {
|
||||
// while in the scope of the composable, we provide the navBackStackEntry as the
|
||||
// ViewModelStoreOwner and LifecycleOwner
|
||||
it.LocalOwnersProvider(saveableStateHolder) {
|
||||
(it.destination as ComposeNavigator.Destination)
|
||||
.content(this, it)
|
||||
}
|
||||
}
|
||||
LaunchedEffect(transition.currentState, transition.targetState) {
|
||||
if (transition.currentState == transition.targetState) {
|
||||
visibleEntries.forEach { entry ->
|
||||
composeNavigator.onTransitionComplete(entry)
|
||||
}
|
||||
zIndices
|
||||
.filter { it.key != transition.targetState.id }
|
||||
.forEach { zIndices.remove(it.key) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val dialogNavigator = navController.navigatorProvider.get<Navigator<out NavDestination>>(
|
||||
DialogNavigator.NAME,
|
||||
) as? DialogNavigator ?: return
|
||||
|
||||
// Show any dialog destinations
|
||||
DialogHost(dialogNavigator)
|
||||
}
|
||||
|
||||
private fun NavDestination.createEnterTransition(
|
||||
scope: AnimatedContentTransitionScope<NavBackStackEntry>,
|
||||
): EnterTransition? = when (this) {
|
||||
is ComposeNavigator.Destination -> this.enterTransition?.invoke(scope)
|
||||
is ComposeNavGraphNavigator.ComposeNavGraph -> this.enterTransition?.invoke(scope)
|
||||
else -> null
|
||||
}
|
||||
|
||||
private fun NavDestination.createExitTransition(
|
||||
scope: AnimatedContentTransitionScope<NavBackStackEntry>,
|
||||
): ExitTransition? = when (this) {
|
||||
is ComposeNavigator.Destination -> this.exitTransition?.invoke(scope)
|
||||
is ComposeNavGraphNavigator.ComposeNavGraph -> this.exitTransition?.invoke(scope)
|
||||
else -> null
|
||||
}
|
||||
|
||||
private fun NavDestination.createPopEnterTransition(
|
||||
scope: AnimatedContentTransitionScope<NavBackStackEntry>,
|
||||
): EnterTransition? = when (this) {
|
||||
is ComposeNavigator.Destination -> this.popEnterTransition?.invoke(scope)
|
||||
is ComposeNavGraphNavigator.ComposeNavGraph -> this.popEnterTransition?.invoke(scope)
|
||||
else -> null
|
||||
}
|
||||
private fun NavDestination.createPopExitTransition(
|
||||
scope: AnimatedContentTransitionScope<NavBackStackEntry>,
|
||||
): ExitTransition? = when (this) {
|
||||
is ComposeNavigator.Destination -> this.popExitTransition?.invoke(scope)
|
||||
is ComposeNavGraphNavigator.ComposeNavGraph -> this.popExitTransition?.invoke(scope)
|
||||
else -> null
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
@file:Suppress("ktlint:standard:property-naming", "ktlint:standard:function-naming")
|
||||
|
||||
package androidx.navigation.compose
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.saveable.Saver
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.navigation.NavBackStackEntry
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavDestination
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.Navigator
|
||||
|
||||
/**
|
||||
* Gets the current navigation back stack entry as a [MutableState]. When the given navController
|
||||
* changes the back stack due to a [NavController.navigate] or [NavController.popBackStack] this
|
||||
* will trigger a recompose and return the top entry on the back stack.
|
||||
*
|
||||
* @return a mutable state of the current back stack entry
|
||||
*/
|
||||
@Composable
|
||||
public fun NavController.currentBackStackEntryAsState(): State<NavBackStackEntry?> {
|
||||
return currentBackStackEntryFlow.collectAsState(null)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a NavHostController that handles the adding of the [ComposeNavigator] and
|
||||
* [DialogNavigator]. Additional [Navigator] instances can be passed through [navigators] to
|
||||
* be applied to the returned NavController. Note that each [Navigator] must be separately
|
||||
* remembered before being passed in here: any changes to those inputs will cause the
|
||||
* NavController to be recreated.
|
||||
*
|
||||
* @see NavHost
|
||||
*/
|
||||
@Composable
|
||||
public fun rememberNavController(
|
||||
vararg navigators: Navigator<out NavDestination>,
|
||||
): NavHostController {
|
||||
val context = LocalContext.current
|
||||
return rememberSaveable(inputs = navigators, saver = NavControllerSaver(context)) {
|
||||
createNavController(context)
|
||||
}.apply {
|
||||
for (navigator in navigators) {
|
||||
navigatorProvider.addNavigator(navigator)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createNavController(context: Context) =
|
||||
NavHostController(context).apply {
|
||||
navigatorProvider.addNavigator(ComposeNavGraphNavigator(navigatorProvider))
|
||||
navigatorProvider.addNavigator(ComposeNavigator())
|
||||
navigatorProvider.addNavigator(DialogNavigator())
|
||||
}
|
||||
|
||||
/**
|
||||
* Saver to save and restore the NavController across config change and process death.
|
||||
*/
|
||||
private fun NavControllerSaver(
|
||||
context: Context,
|
||||
): Saver<NavHostController, *> = Saver<NavHostController, Bundle>(
|
||||
save = { it.saveState() },
|
||||
restore = { createNavController(context).apply { restoreState(it) } },
|
||||
)
|
||||
+38
-125
@@ -56,7 +56,8 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
|
||||
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||
|
||||
// Current quickscale state
|
||||
private val quickScaleThreshold: Float
|
||||
private val quickScaleThreshold =
|
||||
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 20F, context.resources.displayMetrics)
|
||||
|
||||
// Long click handler
|
||||
private var longClickJob: Job? = null
|
||||
@@ -85,9 +86,6 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
|
||||
@ScaleType
|
||||
private var minimumScaleType = SCALE_TYPE_CENTER_INSIDE
|
||||
|
||||
// Whether to crop borders.
|
||||
private var cropBorders = false
|
||||
|
||||
// Gesture detection settings
|
||||
private var panEnabled = true
|
||||
|
||||
@@ -145,7 +143,7 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
|
||||
|
||||
// Min scale allowed (prevent infinite zoom)
|
||||
private var minScale = minScale()
|
||||
private val sRegion = Rect(0, 0, 0, 0)
|
||||
private var srcRect = Rect(0, 0, 0, 0)
|
||||
|
||||
// Is two-finger zooming in progress
|
||||
private var isZooming = false
|
||||
@@ -186,15 +184,6 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
|
||||
var isReady = false
|
||||
private set
|
||||
|
||||
/**
|
||||
* Call to find whether the main image (base layer tiles where relevant) have been loaded. Before
|
||||
* this event the view is blank unless a preview was provided.
|
||||
*
|
||||
* @return true if the main image (not the preview) has been loaded and is ready to display.
|
||||
*/
|
||||
var isImageLoaded = false
|
||||
private set
|
||||
|
||||
// Event listener
|
||||
private var onImageEventListener: OnImageEventListener? = null
|
||||
|
||||
@@ -218,11 +207,7 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
|
||||
private val mtrx = Matrix()
|
||||
|
||||
init {
|
||||
setMinimumDpi(160)
|
||||
setDoubleTapZoomDpi(160)
|
||||
setGestureDetector(context)
|
||||
quickScaleThreshold =
|
||||
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 20F, context.resources.displayMetrics)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -230,8 +215,9 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
|
||||
*
|
||||
* @param bitmap Image source.
|
||||
*/
|
||||
fun setImage(bitmap: Bitmap) {
|
||||
fun setImage(bitmap: Bitmap, rect: Rect) {
|
||||
reset(true)
|
||||
srcRect = Rect(rect)
|
||||
onImageLoaded(bitmap)
|
||||
}
|
||||
|
||||
@@ -267,9 +253,8 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
|
||||
}
|
||||
sWidth = 0
|
||||
sHeight = 0
|
||||
sRegion.setEmpty()
|
||||
srcRect.setEmpty()
|
||||
isReady = false
|
||||
isImageLoaded = false
|
||||
bitmap = null
|
||||
bitmapIsCached = false
|
||||
}
|
||||
@@ -760,25 +745,20 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
|
||||
val xScale = scale
|
||||
val yScale = scale
|
||||
mtrx.reset()
|
||||
mtrx.postTranslate((-srcRect.left).toFloat(), (-srcRect.top).toFloat())
|
||||
mtrx.postScale(xScale, yScale)
|
||||
mtrx.postTranslate(vTranslate!!.x, vTranslate!!.y)
|
||||
canvas.drawBitmap(bitmap!!, mtrx, bitmapPaint)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the base layer of tiles or full size bitmap is ready.
|
||||
*/
|
||||
private val isBaseLayerReady: Boolean
|
||||
get() = bitmap != null
|
||||
|
||||
/**
|
||||
* Check whether view and image dimensions are known and either a preview, full size image or
|
||||
* base layer tiles are loaded. First time, send ready event to listener. The next draw will
|
||||
* display an image.
|
||||
*/
|
||||
private fun checkReady(): Boolean {
|
||||
val ready = width > 0 && height > 0 && sWidth > 0 && sHeight > 0 && (bitmap != null || isBaseLayerReady)
|
||||
val ready = width > 0 && height > 0 && sWidth > 0 && sHeight > 0 && bitmap != null
|
||||
if (!isReady && ready) {
|
||||
preDraw()
|
||||
isReady = true
|
||||
@@ -787,19 +767,6 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
|
||||
return ready
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether either the full size bitmap or base layer tiles are loaded. First time, send image
|
||||
* loaded event to listener.
|
||||
*/
|
||||
private fun checkImageLoaded(): Boolean {
|
||||
val imageLoaded = isBaseLayerReady
|
||||
if (!isImageLoaded && imageLoaded) {
|
||||
preDraw()
|
||||
isImageLoaded = true
|
||||
}
|
||||
return imageLoaded
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets scale and translate ready for the next draw.
|
||||
*/
|
||||
@@ -830,13 +797,14 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
|
||||
* is set so one dimension fills the view and the image is centered on the other dimension. Used to calculate what the target of an
|
||||
* animation should be.
|
||||
*
|
||||
* @param _center Whether the image should be centered in the dimension it's too small to fill. While animating this can be false to avoid changes in direction as bounds are reached.
|
||||
* @param maybeCenter Whether the image should be centered in the dimension it's too small to fill. While animating this can be false to avoid changes in direction as bounds are reached.
|
||||
* @param sat The scale we want and the translation we're aiming for. The values are adjusted to be valid.
|
||||
*/
|
||||
private fun fitToBounds(_center: Boolean, sat: ScaleAndTranslate) {
|
||||
var center = _center
|
||||
if (panLimit == PAN_LIMIT_OUTSIDE && isReady) {
|
||||
center = false
|
||||
private fun fitToBounds(maybeCenter: Boolean, sat: ScaleAndTranslate) {
|
||||
val center = if (panLimit == PAN_LIMIT_OUTSIDE && isReady) {
|
||||
false
|
||||
} else {
|
||||
maybeCenter
|
||||
}
|
||||
val vTranslate = sat.vTranslate
|
||||
val scale = limitedScale(sat.scale)
|
||||
@@ -908,7 +876,7 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
|
||||
@Synchronized
|
||||
private fun onImageLoaded(bitmap: Bitmap) {
|
||||
// If actual dimensions don't match the declared size, reset everything.
|
||||
if (sWidth > 0 && sHeight > 0 && (sWidth != bitmap.width || sHeight != bitmap.height)) {
|
||||
if (sWidth > 0 && sHeight > 0 && (sWidth != srcRect.width() || sHeight != srcRect.height())) {
|
||||
reset(false)
|
||||
}
|
||||
if (!this.bitmapIsCached) {
|
||||
@@ -916,23 +884,11 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
|
||||
}
|
||||
this.bitmapIsCached = true
|
||||
this.bitmap = bitmap
|
||||
sWidth = bitmap.width
|
||||
sHeight = bitmap.height
|
||||
val ready = checkReady()
|
||||
val imageLoaded = checkImageLoaded()
|
||||
if (ready || imageLoaded) {
|
||||
invalidate()
|
||||
requestLayout()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set border crop of non-filled (white or black) content.
|
||||
*
|
||||
* @param cropBorders Whether to crop image borders.
|
||||
*/
|
||||
fun setCropBorders(cropBorders: Boolean) {
|
||||
this.cropBorders = cropBorders
|
||||
sWidth = srcRect.width()
|
||||
sHeight = srcRect.height()
|
||||
checkReady()
|
||||
invalidate()
|
||||
requestLayout()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -956,16 +912,12 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
|
||||
/**
|
||||
* Convert screen to source x coordinate.
|
||||
*/
|
||||
private fun viewToSourceX(vx: Float): Float {
|
||||
return if (vTranslate == null) Float.NaN else (vx - vTranslate!!.x) / scale
|
||||
}
|
||||
private fun viewToSourceX(vx: Float): Float = if (vTranslate == null) Float.NaN else (vx - vTranslate!!.x) / scale
|
||||
|
||||
/**
|
||||
* Convert screen to source y coordinate.
|
||||
*/
|
||||
private fun viewToSourceY(vy: Float): Float {
|
||||
return if (vTranslate == null) Float.NaN else (vy - vTranslate!!.y) / scale
|
||||
}
|
||||
private fun viewToSourceY(vy: Float): Float = if (vTranslate == null) Float.NaN else (vy - vTranslate!!.y) / scale
|
||||
|
||||
/**
|
||||
* Convert screen coordinate to source coordinate.
|
||||
@@ -973,9 +925,7 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
|
||||
* @param vxy view X/Y coordinate.
|
||||
* @return a coordinate representing the corresponding source coordinate.
|
||||
*/
|
||||
fun viewToSourceCoord(vxy: PointF): PointF? {
|
||||
return viewToSourceCoord(vxy.x, vxy.y, PointF())
|
||||
}
|
||||
fun viewToSourceCoord(vxy: PointF): PointF? = viewToSourceCoord(vxy.x, vxy.y, PointF())
|
||||
|
||||
/**
|
||||
* Convert screen coordinate to source coordinate.
|
||||
@@ -985,7 +935,7 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
|
||||
* @param sTarget target object for result. The same instance is also returned.
|
||||
* @return a coordinate representing the corresponding source coordinate. This is the same instance passed to the sTarget param.
|
||||
*/
|
||||
fun viewToSourceCoord(vx: Float, vy: Float, sTarget: PointF = PointF()): PointF? {
|
||||
private fun viewToSourceCoord(vx: Float, vy: Float, sTarget: PointF = PointF()): PointF? {
|
||||
if (vTranslate == null) {
|
||||
return null
|
||||
}
|
||||
@@ -996,16 +946,12 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
|
||||
/**
|
||||
* Convert source to view x coordinate.
|
||||
*/
|
||||
private fun sourceToViewX(sx: Float): Float {
|
||||
return if (vTranslate == null) Float.NaN else sx * scale + vTranslate!!.x
|
||||
}
|
||||
private fun sourceToViewX(sx: Float): Float = if (vTranslate == null) Float.NaN else sx * scale + vTranslate!!.x
|
||||
|
||||
/**
|
||||
* Convert source to view y coordinate.
|
||||
*/
|
||||
private fun sourceToViewY(sy: Float): Float {
|
||||
return if (vTranslate == null) Float.NaN else sy * scale + vTranslate!!.y
|
||||
}
|
||||
private fun sourceToViewY(sy: Float): Float = if (vTranslate == null) Float.NaN else sy * scale + vTranslate!!.y
|
||||
|
||||
/**
|
||||
* Convert source coordinate to view coordinate.
|
||||
@@ -1013,9 +959,7 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
|
||||
* @param sxy source coordinates to convert.
|
||||
* @return view coordinates.
|
||||
*/
|
||||
fun sourceToViewCoord(sxy: PointF?): PointF? {
|
||||
return sourceToViewCoord(sxy!!.x, sxy.y, PointF())
|
||||
}
|
||||
fun sourceToViewCoord(sxy: PointF?): PointF? = sourceToViewCoord(sxy!!.x, sxy.y, PointF())
|
||||
|
||||
/**
|
||||
* Convert source coordinate to view coordinate.
|
||||
@@ -1025,7 +969,7 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
|
||||
* @param vTarget target object for result. The same instance is also returned.
|
||||
* @return view coordinates. This is the same instance passed to the vTarget param.
|
||||
*/
|
||||
fun sourceToViewCoord(sx: Float, sy: Float, vTarget: PointF = PointF()): PointF? {
|
||||
private fun sourceToViewCoord(sx: Float, sy: Float, vTarget: PointF = PointF()): PointF? {
|
||||
if (vTranslate == null) {
|
||||
return null
|
||||
}
|
||||
@@ -1097,9 +1041,7 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
|
||||
/**
|
||||
* Adjust a requested scale to be within the allowed limits.
|
||||
*/
|
||||
private fun limitedScale(targetScale: Float): Float {
|
||||
return targetScale.coerceIn(minScale(), maxScale)
|
||||
}
|
||||
private fun limitedScale(targetScale: Float): Float = targetScale.coerceIn(minScale(), maxScale)
|
||||
|
||||
/**
|
||||
* Apply a selected type of easing.
|
||||
@@ -1111,12 +1053,10 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
|
||||
* @param duration Anm duration
|
||||
* @return Current value
|
||||
*/
|
||||
private fun ease(type: Int, time: Long, from: Float, change: Float, duration: Long): Float {
|
||||
return when (type) {
|
||||
EASE_IN_OUT_QUAD -> easeInOutQuad(time, from, change, duration)
|
||||
EASE_OUT_QUAD -> easeOutQuad(time, from, change, duration)
|
||||
else -> throw IllegalStateException("Unexpected easing type: $type")
|
||||
}
|
||||
private fun ease(type: Int, time: Long, from: Float, change: Float, duration: Long): Float = when (type) {
|
||||
EASE_IN_OUT_QUAD -> easeInOutQuad(time, from, change, duration)
|
||||
EASE_OUT_QUAD -> easeOutQuad(time, from, change, duration)
|
||||
else -> throw IllegalStateException("Unexpected easing type: $type")
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1243,9 +1183,7 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
|
||||
*
|
||||
* @return the minimum scale as a source/view pixels ratio.
|
||||
*/
|
||||
fun getMinScale(): Float {
|
||||
return minScale()
|
||||
}
|
||||
fun getMinScale(): Float = minScale()
|
||||
|
||||
/**
|
||||
* Set the minimum scale allowed. A value of 1 means 1:1 pixels at minimum scale. You may wish to set this according
|
||||
@@ -1334,8 +1272,7 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a listener allowing notification of load and error events. Extend [DefaultOnImageEventListener]
|
||||
* to simplify implementation.
|
||||
* Add a listener allowing notification of load and error events.
|
||||
*
|
||||
* @param onImageEventListener an [OnImageEventListener] instance.
|
||||
*/
|
||||
@@ -1370,9 +1307,7 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
|
||||
* @param sCenter Target center point
|
||||
* @return [AnimationBuilder] instance. Call [SubsamplingScaleImageView.AnimationBuilder.start] to start the anim.
|
||||
*/
|
||||
fun animateCenter(sCenter: PointF): AnimationBuilder? {
|
||||
return if (isReady) AnimationBuilder(sCenter) else null
|
||||
}
|
||||
fun animateCenter(sCenter: PointF): AnimationBuilder? = if (isReady) AnimationBuilder(sCenter) else null
|
||||
|
||||
/**
|
||||
* Creates a scale animation builder, that when started will animate a zoom in or out. If this would move the image
|
||||
@@ -1382,14 +1317,12 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
|
||||
* @param sCenter Target source center.
|
||||
* @return [AnimationBuilder] instance. Call [SubsamplingScaleImageView.AnimationBuilder.start] to start the anim.
|
||||
*/
|
||||
fun animateScaleAndCenter(scale: Float, sCenter: PointF?): AnimationBuilder? {
|
||||
return if (isReady) AnimationBuilder(scale, sCenter) else null
|
||||
}
|
||||
fun animateScaleAndCenter(scale: Float, sCenter: PointF?): AnimationBuilder? = if (isReady) AnimationBuilder(scale, sCenter) else null
|
||||
|
||||
/**
|
||||
* An event listener, allowing subclasses and activities to be notified of significant events.
|
||||
*/
|
||||
interface OnImageEventListener {
|
||||
fun interface OnImageEventListener {
|
||||
/**
|
||||
* Called when the dimensions of the image and view are known, and either a preview image,
|
||||
* the full size image, or base layer tiles are loaded. This indicates the scale and translate
|
||||
@@ -1397,16 +1330,6 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
|
||||
* graphic, or inform a subclass that it is safe to draw overlays.
|
||||
*/
|
||||
fun onReady()
|
||||
|
||||
/**
|
||||
* Indicates an error initiliasing the decoder when using a tiling, or when loading the full
|
||||
* size bitmap when tiling is disabled. This method cannot be relied upon; certain encoding
|
||||
* types of supported image formats can result in corrupt or blank images being loaded and
|
||||
* displayed with no detectable error.
|
||||
*
|
||||
* @param e The exception thrown. This error is also logged by the view.
|
||||
*/
|
||||
fun onImageLoadError(e: Exception)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1465,14 +1388,6 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
|
||||
|
||||
private data class ScaleAndTranslate(var scale: Float, val vTranslate: PointF)
|
||||
|
||||
/**
|
||||
* Default implementation of [OnImageEventListener] for extension. This does nothing in any method.
|
||||
*/
|
||||
open class DefaultOnImageEventListener : OnImageEventListener {
|
||||
override fun onReady() {}
|
||||
override fun onImageLoadError(e: Exception) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder class used to set additional options for a scale animation. Create an instance using [.animateScale],
|
||||
* then set your options and call [.start].
|
||||
@@ -1721,7 +1636,5 @@ open class SubsamplingScaleImageView @JvmOverloads constructor(
|
||||
* State change originated from a double tap zoom anim.
|
||||
*/
|
||||
const val ORIGIN_DOUBLE_TAP_ZOOM = 4
|
||||
|
||||
private val TAG = SubsamplingScaleImageView::class.java.simpleName
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
/*
|
||||
* 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
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,49 +17,61 @@
|
||||
package com.hippo.ehviewer
|
||||
|
||||
import android.app.Application
|
||||
import android.content.ComponentCallbacks2
|
||||
import android.content.Context
|
||||
import android.os.StrictMode
|
||||
import android.util.Log
|
||||
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.decode.ImageDecoderDecoder
|
||||
import coil3.asImage
|
||||
import coil3.gif.AnimatedImageDecoder
|
||||
import coil3.gif.GifDecoder
|
||||
import coil3.network.ktor.KtorNetworkFetcherFactory
|
||||
import coil3.request.ErrorResult
|
||||
import coil3.request.ImageRequest
|
||||
import coil3.request.crossfade
|
||||
import coil3.serviceLoaderEnabled
|
||||
import coil3.util.DebugLogger
|
||||
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.DownloadThumbInterceptor
|
||||
import com.hippo.ehviewer.coil.HardwareBitmapInterceptor
|
||||
import com.hippo.ehviewer.coil.MergeInterceptor
|
||||
import com.hippo.ehviewer.coil.installKtorHttpUriFetcher
|
||||
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.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.ReadableTime
|
||||
import com.hippo.ehviewer.util.isAtLeastO
|
||||
import com.hippo.ehviewer.util.isAtLeastP
|
||||
import com.hippo.ehviewer.util.isAtLeastS
|
||||
import com.hippo.ehviewer.util.isCronetAvailable
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.engine.apache5.Apache5
|
||||
import io.ktor.client.plugins.cookies.HttpCookies
|
||||
import kotlinx.coroutines.launch
|
||||
import logcat.AndroidLogcatLogger
|
||||
import logcat.LogPriority
|
||||
import logcat.LogcatLogger
|
||||
import logcat.asLog
|
||||
import okio.Path.Companion.toOkioPath
|
||||
import splitties.arch.room.roomDb
|
||||
import splitties.init.appCtx
|
||||
@@ -67,7 +79,9 @@ 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() {
|
||||
// Initialize Settings on first access
|
||||
lifecycleScope.launchIO {
|
||||
@@ -77,6 +91,9 @@ class EhApplication : Application(), SingletonImageLoader.Factory {
|
||||
AppCompatDelegate.setDefaultNightMode(mode)
|
||||
}
|
||||
}
|
||||
if (!LogcatLogger.isInstalled && Settings.saveCrashLog) {
|
||||
LogcatLogger.install(AndroidLogcatLogger(LogPriority.VERBOSE))
|
||||
}
|
||||
}
|
||||
lifecycle.addObserver(lockObserver)
|
||||
val handler = Thread.getDefaultUncaughtExceptionHandler()
|
||||
@@ -90,35 +107,37 @@ class EhApplication : Application(), SingletonImageLoader.Factory {
|
||||
handler?.uncaughtException(t, e)
|
||||
}
|
||||
super.onCreate()
|
||||
if (isAtLeastP) {
|
||||
System.loadLibrary("ehviewer")
|
||||
}
|
||||
System.loadLibrary("ehviewer_rust")
|
||||
ReadableTime.initialize(this)
|
||||
System.loadLibrary("ehviewer")
|
||||
lifecycleScope.launchIO {
|
||||
launchIO {
|
||||
EhTagDatabase
|
||||
launch { EhTagDatabase }
|
||||
launch { EhDB }
|
||||
dataStateFlow.value
|
||||
launch {
|
||||
if (DownloadManager.labelList.isNotEmpty() && Settings.downloadFilterMode.key !in Settings.prefs) {
|
||||
Settings.downloadFilterMode.value = DownloadsFilterMode.CUSTOM.flag
|
||||
}
|
||||
DownloadManager.readMetadataFromLocal()
|
||||
}
|
||||
launchIO {
|
||||
EhDB
|
||||
launch {
|
||||
FileUtils.cleanupDirectory(AppConfig.externalCrashDir)
|
||||
FileUtils.cleanupDirectory(AppConfig.externalParseErrorDir)
|
||||
}
|
||||
launchIO {
|
||||
DownloadManager.isIdle
|
||||
}
|
||||
launchIO {
|
||||
launch {
|
||||
cleanupDownload()
|
||||
}
|
||||
if (Settings.requestNews) {
|
||||
launchIO {
|
||||
launch {
|
||||
checkDawn()
|
||||
}
|
||||
}
|
||||
launch {
|
||||
cleanObsoleteCache()
|
||||
}
|
||||
}
|
||||
cleanObsoleteCache(this)
|
||||
if (BuildConfig.DEBUG) {
|
||||
StrictMode.enableDefaults()
|
||||
Snapshot.registerApplyObserver { anies, _ ->
|
||||
logcat(Log.VERBOSE) { anies.toString() }
|
||||
logcat(LogPriority.VERBOSE) { anies.toString() }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -127,12 +146,12 @@ class EhApplication : Application(), SingletonImageLoader.Factory {
|
||||
runCatching {
|
||||
keepNoMediaFileStatus()
|
||||
}.onFailure {
|
||||
it.printStackTrace()
|
||||
logcat(it)
|
||||
}
|
||||
runCatching {
|
||||
clearTempDir()
|
||||
}.onFailure {
|
||||
it.printStackTrace()
|
||||
logcat(it)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,41 +166,47 @@ class EhApplication : Application(), SingletonImageLoader.Factory {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTrimMemory(level: Int) {
|
||||
super.onTrimMemory(level)
|
||||
if (level >= ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW) {
|
||||
galleryDetailCache.evictAll()
|
||||
}
|
||||
}
|
||||
|
||||
override fun newImageLoader() = imageLoader {
|
||||
override fun newImageLoader(context: Context) = context.imageLoader {
|
||||
components {
|
||||
installKtorHttpUriFetcher()
|
||||
if (isAtLeastP) {
|
||||
add { result, options, _ -> ImageDecoderDecoder(result.source, options, false) }
|
||||
}
|
||||
serviceLoaderEnabled(false)
|
||||
add(KtorNetworkFetcherFactory { ktorClient })
|
||||
add(MergeInterceptor)
|
||||
add(DownloadThumbInterceptor)
|
||||
if (isAtLeastO) {
|
||||
add(HardwareBitmapInterceptor)
|
||||
}
|
||||
add(CropBorderInterceptor)
|
||||
if (isAtLeastP) {
|
||||
add(AnimatedImageDecoder.Factory(false))
|
||||
} else {
|
||||
add(GifDecoder.Factory())
|
||||
}
|
||||
}
|
||||
diskCache(imageCache)
|
||||
diskCache { imageCache }
|
||||
crossfade(300)
|
||||
val drawable = AppCompatResources.getDrawable(appCtx, R.drawable.image_failed)
|
||||
if (drawable != null) error(drawable.asCoilImage(true))
|
||||
if (BuildConfig.DEBUG) logger(DebugLogger())
|
||||
if (drawable != null) error(drawable.asImage(true))
|
||||
if (BuildConfig.DEBUG) {
|
||||
logger(DebugLogger())
|
||||
} else {
|
||||
eventListener(object : EventListener() {
|
||||
override fun onError(request: ImageRequest, result: ErrorResult) {
|
||||
logcat("ImageLoader", LogPriority.ERROR) {
|
||||
"🚨 Failed - ${request.data}\n${result.throwable.asLog()}"
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val ktorClient by lazy {
|
||||
if (Settings.enableCronet && isCronetAvailable) {
|
||||
HttpClient(CronetEngine) {
|
||||
install(HttpCookies) {
|
||||
storage = EhCookieStore
|
||||
}
|
||||
HttpClient(Cronet) {
|
||||
engine {
|
||||
client = cronetHttpClient
|
||||
}
|
||||
} else {
|
||||
HttpClient(Apache5) {
|
||||
install(HttpCookies) {
|
||||
storage = EhCookieStore
|
||||
}
|
||||
install(HttpCookies) {
|
||||
storage = EhCookieStore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,11 +17,10 @@ package com.hippo.ehviewer
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.os.ParcelFileDescriptor.MODE_READ_ONLY
|
||||
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
|
||||
@@ -29,14 +28,21 @@ import com.hippo.ehviewer.dao.EhDatabase
|
||||
import com.hippo.ehviewer.dao.Filter
|
||||
import com.hippo.ehviewer.dao.HistoryInfo
|
||||
import com.hippo.ehviewer.dao.LocalFavoriteInfo
|
||||
import com.hippo.ehviewer.dao.ProgressInfo
|
||||
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 kotlinx.coroutines.flow.Flow
|
||||
import okio.Path
|
||||
import okio.Path.Companion.toOkioPath
|
||||
import splitties.arch.room.roomDb
|
||||
|
||||
object EhDB {
|
||||
private val db = roomDb<EhDatabase>("eh.db")
|
||||
private const val DB_NAME = "eh.db"
|
||||
private val db = roomDb<EhDatabase>(DB_NAME) {
|
||||
addMigrations(Schema17to18())
|
||||
}
|
||||
|
||||
private suspend fun putGalleryInfo(galleryInfo: BaseGalleryInfo) {
|
||||
db.galleryDao().upsert(galleryInfo)
|
||||
@@ -46,13 +52,22 @@ object EhDB {
|
||||
runCatching { db.galleryDao().delete(galleryInfo) }
|
||||
}
|
||||
|
||||
suspend fun updateGalleryInfo(galleryInfoList: List<BaseGalleryInfo>) {
|
||||
db.galleryDao().update(galleryInfoList)
|
||||
}
|
||||
|
||||
fun getReadProgressFlow(gid: Long) = db.progressDao().getPageFlow(gid)
|
||||
suspend fun getReadProgress(gid: Long) = db.progressDao().getPage(gid)
|
||||
suspend fun putReadProgress(gid: Long, page: Int) = db.progressDao().upsert(ProgressInfo(gid, page))
|
||||
suspend fun clearProgressInfo() = db.progressDao().deleteAll()
|
||||
|
||||
suspend fun getAllDownloadInfo() = db.downloadsDao().joinList().onEach {
|
||||
if (it.state == DownloadInfo.STATE_WAIT || it.state == DownloadInfo.STATE_DOWNLOAD) {
|
||||
it.state = DownloadInfo.STATE_NONE
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun updateDownloadInfo(downloadInfo: List<DownloadInfo>) {
|
||||
suspend fun updateDownloadInfo(downloadInfo: Collection<DownloadInfo>) {
|
||||
val dao = db.downloadsDao()
|
||||
dao.update(downloadInfo.map(DownloadInfo::downloadInfo))
|
||||
}
|
||||
@@ -65,17 +80,17 @@ object EhDB {
|
||||
suspend fun removeDownloadInfo(downloadInfo: DownloadInfo) {
|
||||
val dao = db.downloadsDao()
|
||||
dao.delete(downloadInfo.downloadInfo)
|
||||
dao.fill(downloadInfo.position)
|
||||
deleteGalleryInfo(downloadInfo.galleryInfo)
|
||||
}
|
||||
|
||||
suspend fun randomLocalFav() = db.localFavoritesDao().random()
|
||||
|
||||
suspend fun removeDownloadInfo(downloadInfo: List<DownloadInfo>) {
|
||||
val dao = db.downloadsDao()
|
||||
downloadInfo.forEach {
|
||||
dao.delete(it.downloadInfo)
|
||||
deleteGalleryInfo(it.galleryInfo)
|
||||
}
|
||||
dao.fill(downloadInfo.first().position, downloadInfo.size)
|
||||
}
|
||||
|
||||
suspend fun getDownloadDirname(gid: Long): String? {
|
||||
@@ -99,6 +114,12 @@ object EhDB {
|
||||
dao.insertOrIgnore(downloadDirnameList)
|
||||
}
|
||||
|
||||
val downloadsCountByLabel
|
||||
get() = db.downloadsDao().countByLabel()
|
||||
|
||||
val downloadsCountByArtist
|
||||
get() = db.downloadsDao().countByArtist()
|
||||
|
||||
suspend fun getAllDownloadLabelList() = db.downloadLabelDao().list()
|
||||
|
||||
suspend fun addDownloadLabel(raw: DownloadLabel): DownloadLabel {
|
||||
@@ -125,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.insert(artists)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun removeLocalFavorites(galleryInfo: BaseGalleryInfo) {
|
||||
db.localFavoritesDao().deleteByKey(galleryInfo.gid)
|
||||
deleteGalleryInfo(galleryInfo)
|
||||
@@ -182,13 +211,15 @@ object EhDB {
|
||||
val historyLazyList
|
||||
get() = db.historyDao().joinListLazy()
|
||||
|
||||
fun searchHistory(keyword: String) = db.historyDao().joinListLazy("*$keyword*")
|
||||
|
||||
val localFavLazyList
|
||||
get() = db.localFavoritesDao().joinListLazy()
|
||||
|
||||
val localFavCount: Flow<Int>
|
||||
get() = db.localFavoritesDao().count()
|
||||
|
||||
fun searchLocalFav(keyword: String) = db.localFavoritesDao().joinListLazy("%$keyword%")
|
||||
fun searchLocalFav(keyword: String) = db.localFavoritesDao().joinListLazy("*$keyword*")
|
||||
|
||||
suspend fun putHistoryInfo(galleryInfo: BaseGalleryInfo) {
|
||||
putGalleryInfo(galleryInfo)
|
||||
@@ -242,82 +273,57 @@ object EhDB {
|
||||
db.filterDao().update(filter)
|
||||
}
|
||||
|
||||
suspend fun exportDB(context: Context, uri: Uri) {
|
||||
val ehExportName = "eh.export.db"
|
||||
resource {
|
||||
context.deleteDatabase(ehExportName)
|
||||
roomDb<EhDatabase>(ehExportName)
|
||||
} release {
|
||||
it.close()
|
||||
context.deleteDatabase(ehExportName)
|
||||
} use { newDb ->
|
||||
db.galleryDao().list().let { newDb.galleryDao().insertOrIgnore(it) }
|
||||
db.downloadLabelDao().list().let { newDb.downloadLabelDao().insert(it) }
|
||||
db.downloadDirnameDao().list().let { newDb.downloadDirnameDao().insertOrIgnore(it) }
|
||||
db.downloadsDao().list().let { newDb.downloadsDao().insert(it) }
|
||||
db.historyDao().list().let { newDb.historyDao().insertOrIgnore(it) }
|
||||
db.quickSearchDao().list().let { newDb.quickSearchDao().insert(it) }
|
||||
db.localFavoritesDao().list().let { newDb.localFavoritesDao().insertOrIgnore(it) }
|
||||
db.filterDao().list().let { newDb.filterDao().insert(it) }
|
||||
newDb.close()
|
||||
val dbFile = context.getDatabasePath(ehExportName)
|
||||
context.contentResolver.openFileDescriptor(uri, "rw")!!.use { toFd ->
|
||||
ParcelFileDescriptor.open(dbFile, MODE_READ_ONLY).use { fromFd ->
|
||||
fromFd sendTo toFd
|
||||
}
|
||||
}
|
||||
}
|
||||
fun exportDB(context: Context, file: Path) {
|
||||
db.query("PRAGMA wal_checkpoint(FULL)", null).use { it.moveToNext() }
|
||||
val dbFile = context.getDatabasePath(DB_NAME)
|
||||
dbFile.toOkioPath() sendTo file
|
||||
}
|
||||
|
||||
suspend fun importDB(context: Context, uri: Uri) {
|
||||
val tempDBName = "tmp.db"
|
||||
resource {
|
||||
context.deleteDatabase(tempDBName)
|
||||
roomDb<EhDatabase>(tempDBName) { createFromInputStream { context.contentResolver.openInputStream(uri) } }
|
||||
roomDb<EhDatabase>(tempDBName) {
|
||||
createFromInputStream { context.contentResolver.openInputStream(uri) }
|
||||
addMigrations(Schema17to18())
|
||||
}
|
||||
} release {
|
||||
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 {
|
||||
val downloadLabelList = oldDB.downloadLabelDao().list()
|
||||
DownloadManager.addDownloadLabel(downloadLabelList)
|
||||
|
||||
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 {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,8 +10,11 @@ import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.remember
|
||||
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
|
||||
@@ -97,6 +100,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)
|
||||
@@ -108,11 +112,19 @@ object Settings : DataStorePreferences(null) {
|
||||
val thumbColumns = intPref("thumb_columns", 3)
|
||||
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 predictiveNavAnim = boolPref("predictive_nav_anim", true)
|
||||
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", "")
|
||||
|
||||
val needSignInFlow: Flow<Boolean>
|
||||
var downloadScheme by stringOrNullPref("image_scheme", null)
|
||||
var downloadAuthority by stringOrNullPref("image_authority", null)
|
||||
var downloadPath by stringOrNullPref("image_path", null)
|
||||
@@ -129,51 +141,44 @@ 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 hardwareBitmapThreshold by intPref("hardware_bitmap_threshold", 16384)
|
||||
var forceEhThumb by boolPref("force_eh_thumb", false)
|
||||
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)
|
||||
var showJpnTitle by boolPref("show_jpn_title", false)
|
||||
var showGalleryPages by boolPref("show_gallery_pages", true)
|
||||
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 qSSaveProgress by boolPref("qs_save_progress", true)
|
||||
var saveParseErrorBody by boolPref("save_parse_error_body", true)
|
||||
var saveCrashLog by boolPref("save_crash_log", true)
|
||||
var security by boolPref("require_unlock", false)
|
||||
var removeImageFiles by boolPref("include_pic", true)
|
||||
var needSignIn by boolPref("need_sign_in", true).also { needSignInFlow = it.valueFlow() }
|
||||
var needSignIn by boolPref("need_sign_in", true)
|
||||
var harmonizeCategoryColor by boolPref("harmonize_category_color", true)
|
||||
var preloadThumbAggressively by boolPref("preload_thumb_aggressively", false)
|
||||
var downloadOriginImage by boolPref("download_origin_image", false)
|
||||
var enableCronet by boolPref("enable_cronet", true)
|
||||
var saveAsCbz by boolPref("save_as_cbz", false)
|
||||
var archiveMetadata by boolPref("archive_metadata", true)
|
||||
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 requestNewsTimerHour by intPref("request_news_timer_hour", -1)
|
||||
var requestNewsTimerMinute by intPref("request_news_timer_minute", -1)
|
||||
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 recentDownloadLabel by stringOrNullPref("recent_download_label", null)
|
||||
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 lastDawnTime by longPref("last_dawn_time", 0)
|
||||
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)
|
||||
var lastUpdateTime by longPref("last_update_time", BuildConfig.COMMIT_TIME)
|
||||
|
||||
// Tachiyomi Reader
|
||||
var newReader by boolPref("new_compose_reader", false)
|
||||
|
||||
val cropBorder = boolPref("crop_borders", false)
|
||||
val colorFilter = boolPref("pref_color_filter_key", false)
|
||||
val colorFilterValue = intPref("color_filter_value", 0)
|
||||
val colorFilterMode = intPref("color_filter_mode", 0)
|
||||
@@ -182,6 +187,28 @@ object Settings : DataStorePreferences(null) {
|
||||
val readingMode = intPref("pref_default_reading_mode_key", ReadingModeType.LEFT_TO_RIGHT.flagValue)
|
||||
val orientationMode = intPref("pref_default_orientation_type_key", OrientationType.FREE.flagValue)
|
||||
val showReaderSeekbar = boolPref("pref_show_reader_seekbar", true)
|
||||
val showPageNumber = boolPref("pref_show_page_number_key", true)
|
||||
val readerTheme = intPref("pref_reader_theme_key", 1)
|
||||
val doubleTapToZoom = boolPref("pref_double_tap_to_zoom", true)
|
||||
val fullscreen = boolPref("fullscreen", true)
|
||||
val cutoutShort = boolPref("cutout_short", true)
|
||||
val keepScreenOn = boolPref("pref_keep_screen_on_key", true)
|
||||
val readerLongTapAction = boolPref("reader_long_tap", true)
|
||||
val pageTransitions = boolPref("pref_enable_transitions_key", true)
|
||||
val readWithVolumeKeys = boolPref("reader_volume_keys", false)
|
||||
val readWithVolumeKeysInterval = intPref("reader_volume_keys_interval", 0)
|
||||
val readWithVolumeKeysInverted = boolPref("reader_volume_keys_inverted", false)
|
||||
val grayScale = boolPref("pref_grayscale", false)
|
||||
val invertedColors = boolPref("pref_inverted_colors", false)
|
||||
val readerWebtoonNav = intPref("reader_navigation_mode_webtoon", 0)
|
||||
val readerPagerNav = intPref("reader_navigation_mode_pager", 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)
|
||||
|
||||
init {
|
||||
if ("CN" == Locale.getDefault().country) {
|
||||
@@ -200,7 +227,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) {
|
||||
@@ -212,7 +239,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,14 +2,13 @@ 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
|
||||
import com.hippo.ehviewer.ui.keepNoMediaFileStatus
|
||||
import com.hippo.ehviewer.util.isAtLeastS
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
@@ -26,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 {
|
||||
@@ -63,12 +53,12 @@ fun updateWhenRequestNewsChanges() {
|
||||
}
|
||||
|
||||
fun updateWhenGallerySiteChanges() {
|
||||
if (!Settings.needSignIn && EhCookieStore.hasSignedIn()) {
|
||||
if (Settings.hasSignedIn.value) {
|
||||
collectScope.launchIO {
|
||||
runCatching {
|
||||
EhEngine.getUConfig()
|
||||
}.onFailure {
|
||||
it.printStackTrace()
|
||||
logcat(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -79,7 +69,7 @@ fun updateWhenTagTranslationChanges() {
|
||||
runCatching {
|
||||
EhTagDatabase.update()
|
||||
}.onFailure {
|
||||
it.printStackTrace()
|
||||
logcat(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,13 +29,9 @@ 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/"
|
||||
|
||||
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(URL_PREFIX_THUMB_E).removePrefix(URL_PREFIX_LARGE_THUMB_EX).removePrefix(URL_PREFIX_THUMB_EX)
|
||||
|
||||
val GalleryPreview.url
|
||||
get() = if (exThumb) {
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
package com.hippo.ehviewer.client
|
||||
|
||||
import android.webkit.CookieManager
|
||||
import androidx.compose.ui.util.fastAny
|
||||
import androidx.compose.ui.util.fastForEach
|
||||
import androidx.compose.ui.util.fastJoinToString
|
||||
import io.ktor.client.plugins.cookies.CookiesStorage
|
||||
import io.ktor.http.Cookie
|
||||
import io.ktor.http.Url
|
||||
@@ -12,18 +9,15 @@ 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"
|
||||
const val KEY_IGNEOUS = "igneous"
|
||||
private const val KEY_STAR = "star"
|
||||
private const val KEY_CONTENT_WARNING = "nw"
|
||||
private const val CONTENT_WARNING_NOT_SHOW = "1"
|
||||
private const val KEY_UTMP_NAME = "__utmp"
|
||||
@@ -32,28 +26,29 @@ object EhCookieStore : CookiesStorage {
|
||||
value = CONTENT_WARNING_NOT_SHOW,
|
||||
)
|
||||
|
||||
fun copyNecessaryCookies() {
|
||||
val cookies = load(Url(EhUrl.HOST_E))
|
||||
cookies.fastForEach {
|
||||
if (it.name == KEY_STAR || it.name == KEY_IPB_MEMBER_ID || it.name == KEY_IPB_PASS_HASH) {
|
||||
manager.setCookie(EhUrl.HOST_EX, it.toString())
|
||||
}
|
||||
}
|
||||
fun clearIgneous() {
|
||||
manager.setCookie(
|
||||
EhUrl.HOST_EX,
|
||||
renderSetCookieHeader(KEY_IGNEOUS, "", maxAge = 0, domain = EhUrl.DOMAIN_EX, path = "/"),
|
||||
)
|
||||
}
|
||||
|
||||
fun addCookie(k: String, v: String, domain: String) {
|
||||
val cookie = Cookie(name = k, value = v, domain = domain, maxAge = Int.MAX_VALUE)
|
||||
val url = if (EhUrl.DOMAIN_E == cookie.domain) EhUrl.HOST_E else EhUrl.HOST_EX
|
||||
manager.setCookie(url, renderSetCookieHeader(cookie))
|
||||
fun getIdentityCookies(): List<Pair<String, String?>> {
|
||||
val eCookies = getCookies(EhUrl.HOST_E)
|
||||
val exCookies = getCookies(EhUrl.HOST_EX)
|
||||
val ipbMemberId = eCookies?.get(KEY_IPB_MEMBER_ID)
|
||||
val ipbPassHash = eCookies?.get(KEY_IPB_PASS_HASH)
|
||||
val igneous = exCookies?.get(KEY_IGNEOUS)
|
||||
return listOf(
|
||||
KEY_IPB_MEMBER_ID to ipbMemberId,
|
||||
KEY_IPB_PASS_HASH to ipbPassHash,
|
||||
KEY_IGNEOUS to igneous,
|
||||
)
|
||||
}
|
||||
|
||||
fun flush() = manager.flush()
|
||||
|
||||
fun getCookieHeader(url: String): String {
|
||||
return load(Url(url)).fastJoinToString("; ") {
|
||||
"${it.name}=${it.value}"
|
||||
}
|
||||
}
|
||||
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) {
|
||||
@@ -64,17 +59,17 @@ object EhCookieStore : CookiesStorage {
|
||||
|
||||
override fun close() = Unit
|
||||
|
||||
private fun getCookies(url: String) = manager.getCookie(url)?.let { parseClientCookiesHeader(it) }
|
||||
|
||||
fun load(url: Url): List<Cookie> {
|
||||
val cookies = manager.getCookie(url.toString()) ?: return emptyList()
|
||||
val checkTips = EhUrl.DOMAIN_E in url.host
|
||||
return parseClientCookiesHeader(cookies)
|
||||
.map { Cookie(it.key, it.value, maxAge = Int.MAX_VALUE) }
|
||||
.filterTo(mutableListOf()) { it.name != KEY_UTMP_NAME }.apply {
|
||||
if (checkTips) {
|
||||
removeAll { it.name == KEY_CONTENT_WARNING }
|
||||
add(sTipsCookie)
|
||||
}
|
||||
return getCookies(url.toString())?.mapTo(mutableListOf()) {
|
||||
Cookie(it.key, it.value)
|
||||
}?.apply {
|
||||
if (checkTips) {
|
||||
add(sTipsCookie)
|
||||
}
|
||||
} ?: if (checkTips) listOf(sTipsCookie) else emptyList()
|
||||
}
|
||||
|
||||
override suspend fun get(requestUrl: Url) = load(requestUrl)
|
||||
|
||||
@@ -17,9 +17,11 @@ 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
|
||||
import com.hippo.ehviewer.EhDB
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.client.data.BaseGalleryInfo
|
||||
@@ -31,12 +33,12 @@ import com.hippo.ehviewer.client.exception.NotLoggedInException
|
||||
import com.hippo.ehviewer.client.exception.ParseException
|
||||
import com.hippo.ehviewer.client.parser.ArchiveParser
|
||||
import com.hippo.ehviewer.client.parser.EventPaneParser
|
||||
import com.hippo.ehviewer.client.parser.FavParserResult
|
||||
import com.hippo.ehviewer.client.parser.FavoritesParser
|
||||
import com.hippo.ehviewer.client.parser.ForumsParser
|
||||
import com.hippo.ehviewer.client.parser.GalleryApiParser
|
||||
import com.hippo.ehviewer.client.parser.GalleryDetailParser
|
||||
import com.hippo.ehviewer.client.parser.GalleryListParser
|
||||
import com.hippo.ehviewer.client.parser.GalleryListResult
|
||||
import com.hippo.ehviewer.client.parser.GalleryNotAvailableParser
|
||||
import com.hippo.ehviewer.client.parser.GalleryPageParser
|
||||
import com.hippo.ehviewer.client.parser.GalleryTokenApiParser
|
||||
@@ -46,56 +48,50 @@ 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 eu.kanade.tachiyomi.util.system.logcat
|
||||
import io.ktor.client.request.forms.append
|
||||
import io.ktor.client.statement.HttpStatement
|
||||
import io.ktor.client.statement.bodyAsChannel
|
||||
import io.ktor.client.statement.bodyAsText
|
||||
import io.ktor.client.statement.request
|
||||
import io.ktor.http.ContentType
|
||||
import io.ktor.utils.io.core.writeFully
|
||||
import io.ktor.utils.io.pool.DirectByteBufferPool
|
||||
import io.ktor.utils.io.pool.useInstance
|
||||
import java.io.File
|
||||
import java.io.RandomAccessFile
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.channels.FileChannel
|
||||
import kotlin.math.ceil
|
||||
import kotlin.system.measureTimeMillis
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlinx.serialization.SerializationException
|
||||
import kotlinx.serialization.json.add
|
||||
import kotlinx.serialization.json.addJsonArray
|
||||
import kotlinx.serialization.json.put
|
||||
import moe.tarsin.coroutines.removeAllSuspend
|
||||
import moe.tarsin.coroutines.runSuspendCatching
|
||||
import okio.buffer
|
||||
import okio.sink
|
||||
import org.jsoup.Jsoup
|
||||
import splitties.init.appCtx
|
||||
|
||||
const val TAG = "EhEngine"
|
||||
|
||||
// https://ehwiki.org/wiki/API#Basics
|
||||
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")
|
||||
RandomAccessFile(file, "rw").use { randomAccessFile ->
|
||||
with(randomAccessFile) {
|
||||
writeUTF(e.message + "\n")
|
||||
onLeft { writeUTF(it) }
|
||||
onRight { channel.write(it) }
|
||||
val file = File(dir, ReadableTime.getFilenamableTime() + ".txt")
|
||||
file.sink().buffer().use { sink ->
|
||||
with(sink) {
|
||||
writeUtf8(e.message + "\n")
|
||||
onLeft { writeUtf8(it) }
|
||||
onRight { write(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -104,7 +100,7 @@ fun rethrowExactly(code: Int, body: Either<String, ByteBuffer>, e: Throwable): N
|
||||
// Don't translate coroutine cancellation
|
||||
if (e is CancellationException) throw e
|
||||
|
||||
// Check sad panda(without panda)
|
||||
// Check sad panda (without panda)
|
||||
val empty = body.fold(
|
||||
{ it.isEmpty() },
|
||||
{ !it.hasRemaining() },
|
||||
@@ -134,6 +130,12 @@ fun rethrowExactly(code: Int, body: Either<String, ByteBuffer>, e: Throwable): N
|
||||
|
||||
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 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)
|
||||
}
|
||||
if (Settings.saveParseErrorBody) body.saveParseError(e)
|
||||
throw EhException(appCtx.getString(R.string.error_parse_error), e)
|
||||
}
|
||||
@@ -144,9 +146,8 @@ fun rethrowExactly(code: Int, body: Either<String, ByteBuffer>, e: Throwable): N
|
||||
|
||||
val httpContentPool = DirectByteBufferPool(8, 0x80000)
|
||||
|
||||
suspend inline fun <T> HttpStatement.fetchUsingAsText(crossinline block: suspend String.() -> T) = execute { response ->
|
||||
logcat(tag = TAG) { response.request.url.toString() }
|
||||
val body = response.bodyAsText()
|
||||
suspend inline fun <T> HttpStatement.fetchUsingAsText(crossinline block: suspend String.() -> T) = executeSafely { response ->
|
||||
val body = response.bodyAsUtf8Text()
|
||||
runSuspendCatching {
|
||||
block(body)
|
||||
}.onFailure {
|
||||
@@ -154,40 +155,44 @@ suspend inline fun <T> HttpStatement.fetchUsingAsText(crossinline block: suspend
|
||||
}.getOrThrow()
|
||||
}
|
||||
|
||||
suspend inline fun <T> HttpStatement.fetchUsingAsByteBuffer(crossinline block: suspend ByteBuffer.() -> T) = execute { response ->
|
||||
logcat(tag = TAG) { response.request.url.toString() }
|
||||
suspend inline fun <T> HttpStatement.fetchUsingAsByteBuffer(crossinline block: suspend ByteBuffer.() -> T) = executeSafely { response ->
|
||||
httpContentPool.useInstance { buffer ->
|
||||
with(response.bodyAsChannel()) { while (!isClosedForRead) readAvailable(buffer) }
|
||||
buffer.flip()
|
||||
runSuspendCatching {
|
||||
block(buffer)
|
||||
}.onFailure {
|
||||
buffer.rewind()
|
||||
rethrowExactly(response.status.value, buffer.right(), it)
|
||||
}.getOrThrow()
|
||||
}
|
||||
}
|
||||
|
||||
object EhEngine {
|
||||
suspend fun getOriginalImageUrl(url: String, referer: String?) = noRedirectEhRequest(url, referer).execute { response ->
|
||||
suspend fun getOriginalImageUrl(url: String, referer: String?) = noRedirectEhRequest(url, referer).executeSafely { response ->
|
||||
val location = response.headers["Location"] ?: throw InsufficientFundsException()
|
||||
location.takeIf { "bounce_login" !in it } ?: throw NotLoggedInException()
|
||||
}
|
||||
|
||||
suspend fun getTorrentList(url: String, gid: Long, token: String?): TorrentResult {
|
||||
suspend fun getTorrentList(url: String, gid: Long, token: String): TorrentResult {
|
||||
val referer = EhUrl.getGalleryDetailUrl(gid, token)
|
||||
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 }
|
||||
|
||||
@@ -198,19 +203,19 @@ 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) {
|
||||
throwable.printStackTrace()
|
||||
ehRequest(url).fetchUsingAsText { check(U_CONFIG_TEXT in this) { "Unable to load config from $url!" } }
|
||||
logcat(throwable)
|
||||
ehRequest(url).fetchUsingAsByteBuffer(UserConfigParser::parse)
|
||||
} else {
|
||||
throw throwable
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getGalleryPage(url: String, gid: Long, token: String?): GalleryPageParser.Result {
|
||||
suspend fun getGalleryPage(url: String, gid: Long, token: String): GalleryPageParser.Result {
|
||||
val referer = EhUrl.getGalleryDetailUrl(gid, token)
|
||||
return ehRequest(url, referer).fetchUsingAsText(GalleryPageParser::parse)
|
||||
}
|
||||
@@ -222,14 +227,14 @@ object EhEngine {
|
||||
suspend fun getGalleryDetail(url: String) = ehRequest(url, EhUrl.referer).fetchUsingAsText {
|
||||
val eventPane = EventPaneParser.parse(this)
|
||||
if (eventPane != null) {
|
||||
Settings.lastDawnTime = Clock.System.now().epochSeconds
|
||||
Settings.lastDawnDays = today
|
||||
showEventNotification(eventPane)
|
||||
}
|
||||
GalleryDetailParser.parse(this)
|
||||
}
|
||||
|
||||
suspend fun getPreviewList(url: String) = ehRequest(url, EhUrl.referer).fetchUsingAsText {
|
||||
GalleryDetailParser.parsePreviewList(this) to GalleryDetailParser.parsePreviewPages(this)
|
||||
GalleryDetailParser.parsePreviewList(this)
|
||||
}
|
||||
|
||||
suspend fun getFavorites(url: String) = ehRequest(url, EhUrl.referer).fetchUsingAsByteBuffer(FavoritesParser::parse)
|
||||
@@ -260,16 +265,21 @@ object EhEngine {
|
||||
append("edit_comment", id.toString())
|
||||
}
|
||||
}
|
||||
}.fetchUsingAsText {
|
||||
val document = Jsoup.parse(this)
|
||||
val elements = document.select("#chd + p")
|
||||
if (elements.size > 0) {
|
||||
throw EhException(elements[0].text())
|
||||
}.executeSafely { response ->
|
||||
// Ktor does not handle POST redirect, we need to do it manually
|
||||
// https://youtrack.jetbrains.com/issue/KTOR-478
|
||||
val location = response.headers["Location"] ?: url
|
||||
ehRequest(location, url).fetchUsingAsText {
|
||||
val document = Jsoup.parse(this)
|
||||
val elements = document.select("#chd + p")
|
||||
if (elements.size > 0) {
|
||||
throw EhException(elements[0].text())
|
||||
}
|
||||
GalleryDetailParser.parseComments(document)
|
||||
}
|
||||
GalleryDetailParser.parseComments(document)
|
||||
}
|
||||
|
||||
suspend fun modifyFavorites(gid: Long, token: String?, dstCat: Int = -1, note: String = "") {
|
||||
suspend fun modifyFavorites(gid: Long, token: String, dstCat: Int = -1, note: String = "") {
|
||||
val catStr: String = when (dstCat) {
|
||||
-1 -> "favdel"
|
||||
in 0..9 -> dstCat.toString()
|
||||
@@ -284,12 +294,14 @@ object EhEngine {
|
||||
append("apply", "Apply Changes")
|
||||
append("update", "1")
|
||||
}
|
||||
}.execute { }
|
||||
}.executeSafely { }
|
||||
}
|
||||
|
||||
suspend fun downloadArchive(gid: Long, token: String?, or: String?, res: String?, isHAtH: Boolean): String? {
|
||||
if (or.isNullOrEmpty()) throw EhException("Invalid form param or: $or")
|
||||
if (res.isNullOrEmpty()) throw EhException("Invalid res: $res")
|
||||
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)
|
||||
val request = ehRequest(url, referer, EhUrl.origin) {
|
||||
@@ -325,7 +337,7 @@ object EhEngine {
|
||||
}
|
||||
}.fetchUsingAsText(HomeParser::parseResetLimits)
|
||||
|
||||
suspend fun modifyFavorites(gidArray: LongArray, srcCat: Int, dstCat: Int): FavoritesParser.Result {
|
||||
suspend fun modifyFavorites(gidArray: LongArray, srcCat: Int, dstCat: Int): FavParserResult {
|
||||
val url = ehUrl(EhUrl.FAV_PATH) {
|
||||
if (FavListUrlBuilder.isValidFavCat(srcCat)) addQueryParameter("favcat", srcCat.toString())
|
||||
}.buildString()
|
||||
@@ -355,18 +367,18 @@ object EhEngine {
|
||||
}.fetchUsingAsText { GalleryPageParser.parse(filterNot { it == '\\' }) }
|
||||
}
|
||||
|
||||
suspend fun rateGallery(apiUid: Long, apiKey: String?, gid: Long, token: String?, rating: Float): RateGalleryResult = ehRequest(EhUrl.apiUrl, EhUrl.getGalleryDetailUrl(gid, token), EhUrl.origin) {
|
||||
suspend fun rateGallery(apiUid: Long, apiKey: String?, gid: Long, token: String, rating: Float): RateGalleryResult = ehRequest(EhUrl.apiUrl, EhUrl.getGalleryDetailUrl(gid, token), EhUrl.origin) {
|
||||
jsonBody {
|
||||
put("method", "rategallery")
|
||||
put("apiuid", apiUid)
|
||||
put("apikey", apiKey)
|
||||
put("apikey", requireNotNull(apiKey))
|
||||
put("gid", gid)
|
||||
put("token", token)
|
||||
put("rating", ceil((rating * 2).toDouble()).toInt())
|
||||
}
|
||||
}.fetchUsingAsText(String::parseAs)
|
||||
|
||||
suspend fun fillGalleryListByApi(galleryInfoList: List<GalleryInfo>, referer: String) =
|
||||
suspend fun fillGalleryListByApi(galleryInfoList: List<GalleryInfo>, referer: String? = null) =
|
||||
galleryInfoList.chunked(MAX_REQUEST_SIZE).chunked(MAX_SEQUENTIAL_REQUESTS).forEachIndexed { index, chunk ->
|
||||
if (index != 0) {
|
||||
delay(REQUEST_INTERVAL)
|
||||
@@ -389,12 +401,12 @@ object EhEngine {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun voteComment(apiUid: Long, apiKey: String?, gid: Long, token: String?, commentId: Long, commentVote: Int): VoteCommentResult =
|
||||
suspend fun voteComment(apiUid: Long, apiKey: String?, gid: Long, token: String, commentId: Long, commentVote: Int): VoteCommentResult =
|
||||
ehRequest(EhUrl.apiUrl, EhUrl.referer, EhUrl.origin) {
|
||||
jsonBody {
|
||||
put("method", "votecomment")
|
||||
put("apiuid", apiUid)
|
||||
put("apikey", apiKey)
|
||||
put("apikey", requireNotNull(apiKey))
|
||||
put("gid", gid)
|
||||
put("token", token)
|
||||
put("comment_id", commentId)
|
||||
@@ -402,12 +414,12 @@ object EhEngine {
|
||||
}
|
||||
}.fetchUsingAsText(String::parseAs)
|
||||
|
||||
suspend fun voteTag(apiUid: Long, apiKey: String?, gid: Long, token: String?, tags: String?, vote: Int) =
|
||||
suspend fun voteTag(apiUid: Long, apiKey: String?, gid: Long, token: String, tags: String, vote: Int) =
|
||||
ehRequest(EhUrl.apiUrl, EhUrl.referer, EhUrl.origin) {
|
||||
jsonBody {
|
||||
put("method", "taggallery")
|
||||
put("apiuid", apiUid)
|
||||
put("apikey", apiKey)
|
||||
put("apikey", requireNotNull(apiKey))
|
||||
put("gid", gid)
|
||||
put("token", token)
|
||||
put("tags", tags)
|
||||
@@ -415,7 +427,7 @@ object EhEngine {
|
||||
}
|
||||
}.fetchUsingAsText(VoteTagParser::parse)
|
||||
|
||||
suspend fun getGalleryToken(gid: Long, gtoken: String?, page: Int) = ehRequest(EhUrl.apiUrl, EhUrl.referer, EhUrl.origin) {
|
||||
suspend fun getGalleryToken(gid: Long, gtoken: String, page: Int) = ehRequest(EhUrl.apiUrl, EhUrl.referer, EhUrl.origin) {
|
||||
jsonBody {
|
||||
put("method", "gtoken")
|
||||
array("pagelist") {
|
||||
@@ -428,42 +440,30 @@ object EhEngine {
|
||||
}
|
||||
}.fetchUsingAsText(GalleryTokenApiParser::parse)
|
||||
|
||||
suspend fun imageSearch(jpeg: File, uss: Boolean, osc: Boolean): GalleryListResult {
|
||||
val location = noRedirectEhRequest(EhUrl.imageSearchUrl, EhUrl.referer, EhUrl.origin) {
|
||||
multipartBody {
|
||||
append("sfile", "a.jpg", ContentType.Image.JPEG, jpeg.length()) {
|
||||
RandomAccessFile(jpeg, "r").use {
|
||||
writeFully(it.channel.map(FileChannel.MapMode.READ_ONLY, 0, it.length()))
|
||||
}
|
||||
}
|
||||
if (uss) append("fs_similar", "on")
|
||||
if (osc) append("fs_covers", "on")
|
||||
append("f_sfile", "File Search")
|
||||
}
|
||||
}.execute { it.headers["Location"] ?: error("Failed to search image!!!") }
|
||||
return ehRequest(location).fetchUsingAsByteBuffer(GalleryListParser::parse).apply {
|
||||
galleryInfoList.fillInfo(EhUrl.imageSearchUrl)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun MutableList<BaseGalleryInfo>.fillInfo(url: String, filter: Boolean = false) = with(EhFilter) {
|
||||
if (filter) removeAllSuspend { filterTitle(it) || filterUploader(it) }
|
||||
val hasTags = any { !it.simpleTags.isNullOrEmpty() }
|
||||
val hasPages = any { it.pages != 0 }
|
||||
val hasRated = any { it.rated }
|
||||
val needApi = filter && needTags() && !hasTags || Settings.showGalleryPages && !hasPages || hasRated
|
||||
val needApi = filter && needTags() && !hasTags || Settings.showGalleryPages.value && !hasPages || hasRated
|
||||
if (needApi) fillGalleryListByApi(this@fillInfo, url)
|
||||
if (filter) removeAllSuspend { filterUploader(it) || filterTag(it) || filterTagNamespace(it) }
|
||||
forEach {
|
||||
if (it.favoriteSlot == GalleryInfo.NOT_FAVORITED && EhDB.containLocalFavorites(it.gid)) {
|
||||
it.favoriteSlot = GalleryInfo.LOCAL_FAVORITED
|
||||
}
|
||||
if (!needApi) it.generateSLang()
|
||||
}
|
||||
}
|
||||
|
||||
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,25 +15,26 @@
|
||||
*/
|
||||
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 eu.kanade.tachiyomi.util.system.logcat
|
||||
import io.ktor.client.request.HttpRequestBuilder
|
||||
import io.ktor.client.request.forms.FormBuilder
|
||||
import io.ktor.client.request.forms.FormDataContent
|
||||
import io.ktor.client.request.forms.MultiPartFormDataContent
|
||||
import io.ktor.client.request.forms.formData
|
||||
import io.ktor.client.request.header
|
||||
import io.ktor.client.request.prepareRequest
|
||||
import io.ktor.client.request.setBody
|
||||
import io.ktor.client.statement.HttpResponse
|
||||
import io.ktor.client.statement.HttpStatement
|
||||
import io.ktor.client.statement.bodyAsText
|
||||
import io.ktor.http.ContentType
|
||||
import io.ktor.http.HttpHeaders
|
||||
import io.ktor.http.HttpMethod
|
||||
import io.ktor.http.ParametersBuilder
|
||||
import io.ktor.http.content.TextContent
|
||||
import io.ktor.http.userAgent
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonArrayBuilder
|
||||
import kotlinx.serialization.json.JsonObjectBuilder
|
||||
@@ -44,13 +45,13 @@ import okio.BufferedSource
|
||||
|
||||
inline fun JsonObjectBuilder.array(name: String, builder: JsonArrayBuilder.() -> Unit) = put(name, buildJsonArray(builder))
|
||||
|
||||
suspend inline fun <reified T> HttpStatement.executeAndParseAs() = execute { it.bodyAsText().parseAs<T>() }
|
||||
suspend inline fun <reified T> HttpStatement.executeAndParseAs() = executeSafely { 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 (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"
|
||||
|
||||
@@ -67,11 +68,12 @@ suspend inline fun ehRequest(
|
||||
url: String,
|
||||
referer: String? = null,
|
||||
origin: String? = null,
|
||||
verbose: Boolean = true,
|
||||
builder: HttpRequestBuilder.() -> Unit = {},
|
||||
) = ktorClient.prepareRequest(url) {
|
||||
applyEhConfig(referer, origin)
|
||||
apply(builder)
|
||||
}
|
||||
}.also { if (verbose) logcat("EhRequest") { url } }
|
||||
|
||||
suspend inline fun noRedirectEhRequest(
|
||||
url: String,
|
||||
@@ -81,6 +83,16 @@ suspend inline fun noRedirectEhRequest(
|
||||
) = noRedirectKtorClient.prepareRequest(url) {
|
||||
applyEhConfig(referer, origin)
|
||||
apply(builder)
|
||||
}.also { logcat("EhRequest") { url } }
|
||||
|
||||
suspend inline fun <T> HttpStatement.executeSafely(
|
||||
crossinline block: suspend (response: HttpResponse) -> T,
|
||||
) = execute { resp ->
|
||||
try {
|
||||
block(resp)
|
||||
} finally {
|
||||
resp.coroutineContext.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
inline fun HttpRequestBuilder.formBody(builder: ParametersBuilder.() -> Unit) {
|
||||
@@ -94,8 +106,3 @@ inline fun HttpRequestBuilder.jsonBody(builder: JsonObjectBuilder.() -> Unit) {
|
||||
val json = buildJsonObject(builder).toString()
|
||||
setBody(TextContent(text = json, contentType = ContentType.Application.Json))
|
||||
}
|
||||
|
||||
fun HttpRequestBuilder.multipartBody(builder: FormBuilder.() -> Unit) {
|
||||
method = HttpMethod.Post
|
||||
setBody(MultiPartFormDataContent(formData(builder)))
|
||||
}
|
||||
@@ -21,10 +21,13 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import com.hippo.ehviewer.EhApplication.Companion.ktorClient
|
||||
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.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
|
||||
@@ -41,8 +44,7 @@ import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import moe.tarsin.coroutines.runSuspendCatching
|
||||
import okio.BufferedSource
|
||||
import okio.HashingSink.Companion.sha1
|
||||
import okio.blackholeSink
|
||||
import okio.Path.Companion.toOkioPath
|
||||
import okio.buffer
|
||||
import okio.source
|
||||
import splitties.init.appCtx
|
||||
@@ -61,9 +63,7 @@ object EhTagDatabase : CoroutineScope {
|
||||
|
||||
var initialized by mutableStateOf(false)
|
||||
|
||||
fun getTranslation(prefix: String? = NAMESPACE_PREFIX, tag: String?): String? {
|
||||
return tagGroups[prefix]?.get(tag)?.trim()?.takeIf { it.isNotEmpty() }
|
||||
}
|
||||
fun getTranslation(prefix: String? = NAMESPACE_PREFIX, tag: String?): String? = tagGroups[prefix]?.get(tag)?.trim()?.ifEmpty { null }
|
||||
|
||||
private fun internalSuggestFlow(
|
||||
tags: Map<String, String>,
|
||||
@@ -105,7 +105,7 @@ object EhTagDatabase : CoroutineScope {
|
||||
val prefix = keyword.dropLast(mKeyword.length)
|
||||
val namespace = mKeyword.substringBefore(':')
|
||||
val tagKeyword = mKeyword.drop(namespace.length + 1)
|
||||
val namespacePrefix = namespaceToPrefix(namespace) ?: namespace
|
||||
val namespacePrefix = TagNamespace(namespace).toPrefix() ?: namespace
|
||||
val tags = tagGroups[namespacePrefix.takeIf { tagKeyword.isNotEmpty() && it != NAMESPACE_PREFIX }]
|
||||
tags?.let {
|
||||
internalSuggestFlow(it, tagKeyword, translate, exactly).collect { (hint, tag) ->
|
||||
@@ -129,57 +129,20 @@ object EhTagDatabase : CoroutineScope {
|
||||
private fun String.containsIgnoreSpace(other: String, ignoreCase: Boolean = true): Boolean =
|
||||
removeSpace().contains(other.removeSpace(), ignoreCase)
|
||||
|
||||
private val NAMESPACE_TO_PREFIX = HashMap<String, String>().also {
|
||||
it["artist"] = "a"
|
||||
it["cosplayer"] = "cos"
|
||||
it["character"] = "c"
|
||||
it["female"] = "f"
|
||||
it["group"] = "g"
|
||||
it["language"] = "l"
|
||||
it["male"] = "m"
|
||||
it["mixed"] = "x"
|
||||
it["other"] = "o"
|
||||
it["parody"] = "p"
|
||||
it["reclass"] = "r"
|
||||
}
|
||||
private fun getMetadata(context: Context): Array<String>? = context.resources.getStringArray(R.array.tag_translation_metadata)
|
||||
.takeIf { it.size == 4 }
|
||||
|
||||
fun namespaceToPrefix(namespace: String): String? {
|
||||
return NAMESPACE_TO_PREFIX[namespace]
|
||||
}
|
||||
fun isTranslatable(context: Context): Boolean = context.resources.getBoolean(R.bool.tag_translatable)
|
||||
|
||||
private fun getMetadata(context: Context): Array<String>? {
|
||||
return context.resources.getStringArray(R.array.tag_translation_metadata)
|
||||
.takeIf { it.size == 4 }
|
||||
}
|
||||
private fun getFileContent(file: File): String? = runCatching {
|
||||
file.source().buffer().use { it.readString(StandardCharsets.UTF_8) }
|
||||
}.getOrNull()
|
||||
|
||||
fun isTranslatable(context: Context): Boolean {
|
||||
return 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 getFileSha1(file: File): String? {
|
||||
return runCatching {
|
||||
file.source().buffer().use { source ->
|
||||
sha1(blackholeSink()).use {
|
||||
source.readAll(it)
|
||||
it.hash.hex()
|
||||
}
|
||||
}
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
private fun checkData(sha1: String?, data: File): Boolean {
|
||||
return sha1 != null && sha1 == getFileSha1(data)
|
||||
}
|
||||
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).execute {
|
||||
client.prepareGet(url).executeSafely {
|
||||
if (it.status.isSuccess()) {
|
||||
it.bodyAsChannel().copyTo(file)
|
||||
} else {
|
||||
@@ -213,7 +176,7 @@ object EhTagDatabase : CoroutineScope {
|
||||
runSuspendCatching {
|
||||
issueUpdateInMemoryData()
|
||||
}.onFailure {
|
||||
it.printStackTrace()
|
||||
logcat(it)
|
||||
}
|
||||
updateInternal()
|
||||
}
|
||||
@@ -272,7 +235,7 @@ object EhTagDatabase : CoroutineScope {
|
||||
// Read new EhTagDatabase
|
||||
issueUpdateInMemoryData(dataFile)
|
||||
}.onFailure {
|
||||
it.printStackTrace()
|
||||
logcat(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,18 +52,11 @@ object EhUrl {
|
||||
const val REFERER_E = "https://$DOMAIN_E"
|
||||
const val ORIGIN_E = REFERER_E
|
||||
|
||||
val host: String
|
||||
get() = when (Settings.gallerySite) {
|
||||
SITE_E -> HOST_E
|
||||
SITE_EX -> HOST_EX
|
||||
else -> HOST_E
|
||||
}
|
||||
|
||||
val domain: String
|
||||
get() = when (Settings.gallerySite) {
|
||||
SITE_E -> DOMAIN_E
|
||||
SITE_EX -> DOMAIN_EX
|
||||
else -> HOST_E
|
||||
else -> DOMAIN_E
|
||||
}
|
||||
|
||||
val apiUrl: String
|
||||
@@ -119,39 +112,48 @@ object EhUrl {
|
||||
else -> URL_IMAGE_SEARCH_E
|
||||
}
|
||||
|
||||
fun getGalleryDetailUrl(gid: Long, token: String?): String {
|
||||
return getGalleryDetailUrl(gid, token, 0, false)
|
||||
}
|
||||
fun getGalleryDetailUrl(gid: Long, token: String, index: Int = 0, allComment: Boolean = false) =
|
||||
ehUrl(listOf("g", "$gid", token)) {
|
||||
if (index != 0) addQueryParameter("p", "$index")
|
||||
if (allComment) addQueryParameter("hc", "1")
|
||||
}.buildString()
|
||||
|
||||
fun getGalleryDetailUrl(gid: Long, token: String?, index: Int, allComment: Boolean) = ehUrl(listOf("g", gid.toString(), token.orEmpty())) {
|
||||
if (index != 0) addQueryParameter("p", "$index")
|
||||
if (allComment) addQueryParameter("hc", "1")
|
||||
fun getGalleryMultiPageViewerUrl(gid: Long, token: String) = ehUrl(listOf("mpv", "$gid", token)).buildString()
|
||||
|
||||
fun getPageUrl(gid: Long, index: Int, pToken: String, nl: String? = null) = ehUrl(listOf("s", pToken, "$gid-${index + 1}")) {
|
||||
addQueryParameterIfNotBlank("nl", nl)
|
||||
}.buildString()
|
||||
|
||||
fun getGalleryMultiPageViewerUrl(gid: Long, token: String) = ehUrl(listOf("mpv", gid.toString(), token)).buildString()
|
||||
fun getAddFavorites(gid: Long, token: String) = ehUrl("gallerypopups.php") {
|
||||
addQueryParameter("gid", "$gid")
|
||||
addQueryParameter("t", token)
|
||||
addQueryParameter("act", "addfav")
|
||||
}.buildString()
|
||||
|
||||
fun getPageUrl(gid: Long, index: Int, pToken: String) = host + "s/" + pToken + '/' + gid + '-' + (index + 1)
|
||||
|
||||
fun getAddFavorites(gid: Long, token: String?) = host + "gallerypopups.php?gid=" + gid + "&t=" + token + "&act=addfav"
|
||||
|
||||
fun getDownloadArchive(gid: Long, token: String?, or: String) = host + "archiver.php?gid=" + gid + "&token=" + token + "&or=" + or
|
||||
fun getDownloadArchive(gid: Long, token: String, or: String) = ehUrl("archiver.php") {
|
||||
addQueryParameter("gid", "$gid")
|
||||
addQueryParameter("token", token)
|
||||
addQueryParameter("or", or)
|
||||
}.buildString()
|
||||
|
||||
fun getTagDefinitionUrl(tag: String) = "https://ehwiki.org/wiki/" + tag.replace(' ', '_')
|
||||
}
|
||||
|
||||
inline fun ehUrl(path: String?, builder: ParametersBuilder.() -> Unit) = ehUrl(
|
||||
inline fun ehUrl(path: String? = null, host: String = EhUrl.domain, builder: ParametersBuilder.() -> Unit) = ehUrl(
|
||||
path = if (path.isNullOrBlank()) emptyList() else listOf(path),
|
||||
host = host,
|
||||
parameters = Parameters.build(builder),
|
||||
)
|
||||
|
||||
inline fun ehUrl(path: List<String>, builder: ParametersBuilder.() -> Unit) = ehUrl(
|
||||
inline fun ehUrl(path: List<String>, host: String = EhUrl.domain, builder: ParametersBuilder.() -> Unit) = ehUrl(
|
||||
path = path,
|
||||
host = host,
|
||||
parameters = Parameters.build(builder),
|
||||
)
|
||||
|
||||
fun ehUrl(path: List<String>, parameters: Parameters = Parameters.Empty) = URLBuilder(
|
||||
fun ehUrl(path: List<String>, host: String = EhUrl.domain, parameters: Parameters = Parameters.Empty) = URLBuilder(
|
||||
protocol = URLProtocol.HTTPS,
|
||||
host = EhUrl.domain,
|
||||
host = host,
|
||||
pathSegments = path,
|
||||
parameters = parameters,
|
||||
)
|
||||
|
||||
@@ -15,19 +15,17 @@
|
||||
*/
|
||||
package com.hippo.ehviewer.client
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.ReadOnlyComposable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.core.graphics.ColorUtils
|
||||
import arrow.core.memoize
|
||||
import com.google.android.material.color.utilities.Hct
|
||||
import com.google.android.material.color.utilities.MathUtils
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.client.data.GalleryInfo
|
||||
import java.util.regex.Pattern
|
||||
import kotlin.math.abs
|
||||
|
||||
object EhUtils {
|
||||
const val NONE = -1 // Use it for homepage
|
||||
@@ -59,14 +57,14 @@ object EhUtils {
|
||||
private const val BG_COLOR_UNKNOWN = 0xff000000u
|
||||
|
||||
// Remove [XXX], (XXX), {XXX}, ~XXX~ stuff
|
||||
private val PATTERN_TITLE_PREFIX = Pattern.compile(
|
||||
"^(?:\\([^)]*\\)|\\[[^]]*]|\\{[^}]*\\}|~[^~]*~|\\s+)*",
|
||||
private val PATTERN_TITLE_PREFIX = Regex(
|
||||
"""^(?:\([^)]*\)|\[[^]]*]|\{[^}]*\}|~[^~]*~|\s+)*""",
|
||||
)
|
||||
|
||||
// Remove [XXX], (XXX), {XXX}, ~XXX~ stuff and something like ch. 1-23
|
||||
private val PATTERN_TITLE_SUFFIX = Pattern.compile(
|
||||
"(?:\\s+ch.[\\s\\d-]+)?(?:\\([^)]*\\)|\\[[^]]*]|\\{[^}]*\\}|~[^~]*~|\\s+)*$",
|
||||
Pattern.CASE_INSENSITIVE,
|
||||
private val PATTERN_TITLE_SUFFIX = Regex(
|
||||
"""(?:\s+ch.[\s\d-]+)?(?:\([^)]*\)|\[[^]]*]|\{[^}]*\}|~[^~]*~|\s+)*$""",
|
||||
RegexOption.IGNORE_CASE,
|
||||
)
|
||||
|
||||
private val CATEGORY_VALUES = hashMapOf(
|
||||
@@ -90,26 +88,36 @@ 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 = 180.0f - abs(abs(a - b) - 180.0f)
|
||||
|
||||
private fun sanitizeDegreesDouble(degrees: Float): Float {
|
||||
val deg = degrees % 360.0f
|
||||
return if (deg < 0) deg + 360.0f else deg
|
||||
}
|
||||
|
||||
private fun rotationDirection(from: Float, to: Float): Float {
|
||||
val increasingDifference = sanitizeDegreesDouble(to - from)
|
||||
return if (increasingDifference <= 180.0) 1.0f else -1.0f
|
||||
}
|
||||
|
||||
@SuppressLint("RestrictedApi")
|
||||
val harmonizeWithRole = { primaryContainer: Int, src: Int ->
|
||||
val fromHct = Hct.fromInt(src)
|
||||
val toHct = Hct.fromInt(primaryContainer)
|
||||
val differenceDegrees = MathUtils.differenceDegrees(fromHct.hue, toHct.hue)
|
||||
val rotationDegrees = minOf(differenceDegrees * 0.5, 15.0)
|
||||
val outputHue = MathUtils.sanitizeDegreesDouble(fromHct.hue + rotationDegrees * MathUtils.rotationDirection(fromHct.hue, toHct.hue))
|
||||
Hct.from(outputHue, toHct.chroma, toHct.tone).toInt()
|
||||
val fromHct = FloatArray(3).apply { ColorUtils.colorToM3HCT(src, this) }
|
||||
val toHct = FloatArray(3).apply { ColorUtils.colorToM3HCT(primaryContainer, this) }
|
||||
val differenceDegrees = differenceDegrees(fromHct[0], toHct[0])
|
||||
val rotationDegrees = minOf(differenceDegrees * 0.5f, 15.0f)
|
||||
val outputHue = sanitizeDegreesDouble(fromHct[0] + rotationDegrees * rotationDirection(fromHct[0], toHct[0]))
|
||||
ColorUtils.M3HCTToColor(outputHue, toHct[1], toHct[2])
|
||||
}.memoize()
|
||||
|
||||
@Stable
|
||||
@@ -141,33 +149,27 @@ 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
|
||||
title = PATTERN_TITLE_PREFIX.matcher(title).replaceFirst("")
|
||||
title = PATTERN_TITLE_SUFFIX.matcher(title).replaceFirst("")
|
||||
title = PATTERN_TITLE_PREFIX.replaceFirst(title, "")
|
||||
title = PATTERN_TITLE_SUFFIX.replaceFirst(title, "")
|
||||
// Sometimes title is combined by romaji and english translation.
|
||||
// Only need romaji.
|
||||
// TODO But not sure every '|' means that
|
||||
val index = title.indexOf('|')
|
||||
if (index >= 0) {
|
||||
title = title.substring(0, index)
|
||||
}
|
||||
return title.ifEmpty { null }
|
||||
return title.substringBeforeLast('|').trim().ifEmpty { null }
|
||||
}
|
||||
|
||||
fun handleThumbUrlResolution(url: String?): String? {
|
||||
|
||||
@@ -5,7 +5,6 @@ interface AbstractDownloadInfo {
|
||||
var legacy: Int
|
||||
var time: Long
|
||||
var label: String?
|
||||
var position: Int
|
||||
var speed: Long
|
||||
var remaining: Long
|
||||
var finished: Int
|
||||
|
||||
@@ -16,23 +16,27 @@
|
||||
package com.hippo.ehviewer.client.data
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.annotation.Keep
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Ignore
|
||||
import androidx.room.PrimaryKey
|
||||
import androidx.room.TypeConverter
|
||||
import androidx.room.TypeConverters
|
||||
import com.hippo.ehviewer.client.data.GalleryInfo.Companion.NOT_FAVORITED
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Parcelize
|
||||
@Serializable
|
||||
@Entity(tableName = "GALLERIES")
|
||||
data class BaseGalleryInfo @Keep constructor(
|
||||
@TypeConverters(SimpleTagsConverter::class)
|
||||
data class BaseGalleryInfo(
|
||||
@PrimaryKey
|
||||
@ColumnInfo(name = "GID")
|
||||
override var gid: Long = 0,
|
||||
override var gid: Long,
|
||||
|
||||
@ColumnInfo(name = "TOKEN")
|
||||
override var token: String? = null,
|
||||
override var token: String,
|
||||
|
||||
@ColumnInfo(name = "TITLE")
|
||||
override var title: String? = null,
|
||||
@@ -61,10 +65,10 @@ data class BaseGalleryInfo @Keep constructor(
|
||||
@Ignore
|
||||
override var rated: Boolean = false,
|
||||
|
||||
@Ignore
|
||||
override var simpleTags: ArrayList<String>? = null,
|
||||
@ColumnInfo(name = "SIMPLE_TAGS")
|
||||
override var simpleTags: List<String>? = null,
|
||||
|
||||
@Ignore
|
||||
@ColumnInfo(name = "PAGES", defaultValue = "0")
|
||||
override var pages: Int = 0,
|
||||
|
||||
@Ignore
|
||||
@@ -84,6 +88,15 @@ data class BaseGalleryInfo @Keep constructor(
|
||||
|
||||
@Ignore
|
||||
override var favoriteNote: String? = null,
|
||||
) : GalleryInfo, Parcelable {
|
||||
constructor() : this(0)
|
||||
) : GalleryInfo,
|
||||
Parcelable {
|
||||
constructor() : this(0, "")
|
||||
}
|
||||
|
||||
class SimpleTagsConverter {
|
||||
@TypeConverter
|
||||
fun fromString(value: String) = value.split(", ")
|
||||
|
||||
@TypeConverter
|
||||
fun toString(value: List<String>) = value.joinToString()
|
||||
}
|
||||
@@ -45,7 +45,7 @@ data class GalleryDetail(
|
||||
var torrentUrl: String? = null,
|
||||
var archiveUrl: String? = null,
|
||||
var parent: String? = null,
|
||||
var newerVersions: ArrayList<BaseGalleryInfo> = ArrayList(),
|
||||
var newerVersions: List<BaseGalleryInfo> = emptyList(),
|
||||
var visible: String? = null,
|
||||
var language: String? = null,
|
||||
var size: String? = null,
|
||||
@@ -55,9 +55,11 @@ data class GalleryDetail(
|
||||
var comments: GalleryCommentList,
|
||||
val previewPages: Int,
|
||||
val previewList: List<GalleryPreview>,
|
||||
) : GalleryInfo by galleryInfo, Parcelable {
|
||||
override fun generateSLang() {
|
||||
) : GalleryInfo by galleryInfo,
|
||||
Parcelable {
|
||||
fun fillInfo() {
|
||||
val index = LANGUAGES.indexOf(language)
|
||||
if (index != -1) simpleLanguage = S_LANGS[index]
|
||||
simpleTags = tags.flatten()
|
||||
}
|
||||
}
|
||||
@@ -15,12 +15,11 @@
|
||||
*/
|
||||
package com.hippo.ehviewer.client.data
|
||||
|
||||
import java.util.regex.Pattern
|
||||
import moe.tarsin.kt.unreachable
|
||||
|
||||
interface GalleryInfo {
|
||||
var gid: Long
|
||||
var token: String?
|
||||
var token: String
|
||||
var title: String?
|
||||
var titleJpn: String?
|
||||
var thumbKey: String?
|
||||
@@ -30,7 +29,7 @@ interface GalleryInfo {
|
||||
var disowned: Boolean
|
||||
var rating: Float
|
||||
var rated: Boolean
|
||||
var simpleTags: ArrayList<String>?
|
||||
var simpleTags: List<String>?
|
||||
var pages: Int
|
||||
var thumbWidth: Int
|
||||
var thumbHeight: Int
|
||||
@@ -62,36 +61,36 @@ interface GalleryInfo {
|
||||
"NL",
|
||||
)
|
||||
private val S_LANG_PATTERNS = arrayOf(
|
||||
Pattern.compile(
|
||||
Regex(
|
||||
"[(\\[]eng(?:lish)?[)\\]]|英訳",
|
||||
Pattern.CASE_INSENSITIVE,
|
||||
RegexOption.IGNORE_CASE,
|
||||
),
|
||||
// [((\[]ch(?:inese)?[))\]]|[汉漢]化|中[国國][语語]|中文|中国翻訳
|
||||
Pattern.compile(
|
||||
Regex(
|
||||
"[(\uFF08\\[]ch(?:inese)?[)\uFF09\\]]|[汉漢]化|中[国國][语語]|中文|中国翻訳",
|
||||
Pattern.CASE_INSENSITIVE,
|
||||
RegexOption.IGNORE_CASE,
|
||||
),
|
||||
Pattern.compile(
|
||||
Regex(
|
||||
"[(\\[]spanish[)\\]]|[(\\[]Español[)\\]]|スペイン翻訳",
|
||||
Pattern.CASE_INSENSITIVE,
|
||||
RegexOption.IGNORE_CASE,
|
||||
),
|
||||
Pattern.compile("[(\\[]korean?[)\\]]|韓国翻訳", Pattern.CASE_INSENSITIVE),
|
||||
Pattern.compile("[(\\[]rus(?:sian)?[)\\]]|ロシア翻訳", Pattern.CASE_INSENSITIVE),
|
||||
Pattern.compile("[(\\[]fr(?:ench)?[)\\]]|フランス翻訳", Pattern.CASE_INSENSITIVE),
|
||||
Pattern.compile("[(\\[]portuguese|ポルトガル翻訳", Pattern.CASE_INSENSITIVE),
|
||||
Pattern.compile(
|
||||
Regex("[(\\[]korean?[)\\]]|韓国翻訳", RegexOption.IGNORE_CASE),
|
||||
Regex("[(\\[]rus(?:sian)?[)\\]]|ロシア翻訳", RegexOption.IGNORE_CASE),
|
||||
Regex("[(\\[]fr(?:ench)?[)\\]]|フランス翻訳", RegexOption.IGNORE_CASE),
|
||||
Regex("[(\\[]portuguese|ポルトガル翻訳", RegexOption.IGNORE_CASE),
|
||||
Regex(
|
||||
"[(\\[]thai(?: ภาษาไทย)?[)\\]]|แปลไทย|タイ翻訳",
|
||||
Pattern.CASE_INSENSITIVE,
|
||||
RegexOption.IGNORE_CASE,
|
||||
),
|
||||
Pattern.compile("[(\\[]german[)\\]]|ドイツ翻訳", Pattern.CASE_INSENSITIVE),
|
||||
Pattern.compile("[(\\[]italiano?[)\\]]|イタリア翻訳", Pattern.CASE_INSENSITIVE),
|
||||
Pattern.compile(
|
||||
Regex("[(\\[]german[)\\]]|ドイツ翻訳", RegexOption.IGNORE_CASE),
|
||||
Regex("[(\\[]italiano?[)\\]]|イタリア翻訳", RegexOption.IGNORE_CASE),
|
||||
Regex(
|
||||
"[(\\[]vietnamese(?: Tiếng Việt)?[)\\]]|ベトナム翻訳",
|
||||
Pattern.CASE_INSENSITIVE,
|
||||
RegexOption.IGNORE_CASE,
|
||||
),
|
||||
Pattern.compile("[(\\[]polish[)\\]]|ポーランド翻訳", Pattern.CASE_INSENSITIVE),
|
||||
Pattern.compile("[(\\[]hun(?:garian)?[)\\]]|ハンガリー翻訳", Pattern.CASE_INSENSITIVE),
|
||||
Pattern.compile("[(\\[]dutch[)\\]]|オランダ翻訳", Pattern.CASE_INSENSITIVE),
|
||||
Regex("[(\\[]polish[)\\]]|ポーランド翻訳", RegexOption.IGNORE_CASE),
|
||||
Regex("[(\\[]hun(?:garian)?[)\\]]|ハンガリー翻訳", RegexOption.IGNORE_CASE),
|
||||
Regex("[(\\[]dutch[)\\]]|オランダ翻訳", RegexOption.IGNORE_CASE),
|
||||
)
|
||||
val S_LANG_TAGS = arrayOf(
|
||||
"language:english",
|
||||
@@ -123,7 +122,7 @@ interface GalleryInfo {
|
||||
|
||||
private fun generateSLangFromTitle(title: String): String? {
|
||||
for (i in S_LANGS.indices) {
|
||||
if (S_LANG_PATTERNS[i].matcher(title).find()) {
|
||||
if (S_LANG_PATTERNS[i].containsMatchIn(title)) {
|
||||
return S_LANGS[i]
|
||||
}
|
||||
}
|
||||
@@ -135,14 +134,10 @@ 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
|
||||
@@ -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
|
||||
@@ -25,7 +25,6 @@ import com.hippo.ehviewer.client.addQueryParameterIfNotBlank
|
||||
import com.hippo.ehviewer.client.ehUrl
|
||||
import com.hippo.ehviewer.dao.QuickSearch
|
||||
import com.hippo.ehviewer.ui.main.AdvanceTable
|
||||
import com.hippo.ehviewer.util.encodeUTF8
|
||||
import com.hippo.ehviewer.util.toIntOrDefault
|
||||
import java.io.UnsupportedEncodingException
|
||||
import java.net.URLDecoder
|
||||
@@ -39,27 +38,35 @@ data class ListUrlBuilder(
|
||||
var mNext: String? = null,
|
||||
// Reset to null after initial loading
|
||||
var mJumpTo: String? = null,
|
||||
private var mRange: Int = 0,
|
||||
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,
|
||||
var pageTo: Int = -1,
|
||||
var imagePath: String? = null,
|
||||
var isUseSimilarityScan: Boolean = false,
|
||||
var isOnlySearchCovers: Boolean = false,
|
||||
) : Parcelable {
|
||||
|
||||
fun setIndex(index: String?, isNext: Boolean = true) {
|
||||
mNext = index?.takeIf { isNext }
|
||||
mPrev = index?.takeUnless { isNext }
|
||||
fun setIndex(index: String, isNext: Boolean = true) {
|
||||
mNext = index.takeIf { isNext }
|
||||
mPrev = index.takeUnless { isNext }
|
||||
mRange = 0
|
||||
}
|
||||
|
||||
fun setJumpTo(jumpTo: Int) {
|
||||
mJumpTo = jumpTo.takeUnless { it == 0 }?.toString()
|
||||
}
|
||||
|
||||
fun setRange(range: Int) {
|
||||
mRange = range
|
||||
mPrev = null
|
||||
mNext = null
|
||||
mJumpTo = null
|
||||
}
|
||||
|
||||
var keyword: String?
|
||||
get() = if (MODE_UPLOADER == mode) "uploader:$mKeyword" else mKeyword
|
||||
set(keyword) {
|
||||
@@ -74,21 +81,19 @@ data class ListUrlBuilder(
|
||||
minRating = q.minRating
|
||||
pageFrom = q.pageFrom
|
||||
pageTo = q.pageTo
|
||||
mNext = q.name.substringAfterLast('@', "").takeIf { it.isNotEmpty() }
|
||||
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) {
|
||||
@@ -253,112 +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)
|
||||
addQueryParameterIfNotBlank("f_shash", hash)
|
||||
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)
|
||||
// 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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}.toString()
|
||||
|
||||
MODE_UPLOADER -> {
|
||||
buildString {
|
||||
append(EhUrl.host)
|
||||
mKeyword?.let {
|
||||
append("uploader/").append(encodeUTF8(it))
|
||||
}
|
||||
mPrev?.let {
|
||||
append("?prev=").append(it)
|
||||
}
|
||||
mNext?.let {
|
||||
append("?next=").append(it)
|
||||
}
|
||||
mJumpTo?.let {
|
||||
append("&seek=").append(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MODE_TAG -> {
|
||||
buildString {
|
||||
append(EhUrl.host)
|
||||
mKeyword?.let {
|
||||
append("tag/").append(encodeUTF8(it))
|
||||
}
|
||||
mPrev?.let {
|
||||
append("?prev=").append(it)
|
||||
}
|
||||
mNext?.let {
|
||||
append("?next=").append(it)
|
||||
}
|
||||
mJumpTo?.let {
|
||||
append("&seek=").append(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MODE_WHATS_HOT -> EhUrl.popularUrl
|
||||
MODE_IMAGE_SEARCH -> EhUrl.imageSearchUrl
|
||||
MODE_TOPLIST -> {
|
||||
buildString {
|
||||
append(EhUrl.HOST_E)
|
||||
append("toplist.php?tl=")
|
||||
append(mKeyword!!)
|
||||
mJumpTo?.let {
|
||||
append("&p=").append(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
else -> throw IllegalStateException("Unexpected value: $mode")
|
||||
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")
|
||||
}
|
||||
|
||||
@IntDef(
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.hippo.ehviewer.client.data
|
||||
|
||||
@JvmInline
|
||||
value class TagNamespace(val value: String) {
|
||||
fun toPrefix(): String? = prefixMap[this]
|
||||
|
||||
companion object {
|
||||
val Artist = TagNamespace("artist")
|
||||
val Cosplayer = TagNamespace("cosplayer")
|
||||
val Character = TagNamespace("character")
|
||||
val Female = TagNamespace("female")
|
||||
val Group = TagNamespace("group")
|
||||
val Language = TagNamespace("language")
|
||||
val Male = TagNamespace("male")
|
||||
val Mixed = TagNamespace("mixed")
|
||||
val Other = TagNamespace("other")
|
||||
val Parody = TagNamespace("parody")
|
||||
val Reclass = TagNamespace("reclass")
|
||||
private val prefixMap = mapOf(
|
||||
Artist to "a",
|
||||
Cosplayer to "cos",
|
||||
Character to "c",
|
||||
Female to "f",
|
||||
Group to "g",
|
||||
Language to "l",
|
||||
Male to "m",
|
||||
Mixed to "x",
|
||||
Other to "o",
|
||||
Parody to "p",
|
||||
Reclass to "r",
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
package com.hippo.ehviewer.client.exception
|
||||
|
||||
import com.hippo.ehviewer.R
|
||||
|
||||
class CloudflareBypassException : EhException(R.string.cloudflare_bypass_failed)
|
||||
@@ -17,6 +17,7 @@ package com.hippo.ehviewer.client.parser
|
||||
|
||||
import com.hippo.ehviewer.client.exception.InsufficientFundsException
|
||||
import com.hippo.ehviewer.client.exception.NoHAtHClientException
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import org.jsoup.Jsoup
|
||||
|
||||
object ArchiveParser {
|
||||
@@ -33,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>()
|
||||
@@ -48,7 +49,7 @@ object ArchiveParser {
|
||||
}.onSuccess {
|
||||
archiveList.add(it)
|
||||
}.onFailure {
|
||||
it.printStackTrace()
|
||||
logcat(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -58,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? {
|
||||
@@ -79,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,
|
||||
)
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.hippo.ehviewer.client.parser
|
||||
|
||||
import io.ktor.util.moveToByteArray
|
||||
import java.nio.ByteBuffer
|
||||
import kotlinx.serialization.cbor.Cbor
|
||||
import kotlinx.serialization.decodeFromByteArray
|
||||
|
||||
inline fun <reified T> unmarshalParsingAs(body: ByteBuffer, parser: (ByteBuffer, Int) -> Int): T {
|
||||
val cborBytes = parser(body, body.limit())
|
||||
body.limit(cborBytes)
|
||||
val array = body.moveToByteArray()
|
||||
return Cbor.decodeFromByteArray<T>(array)
|
||||
}
|
||||
@@ -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,32 +1,33 @@
|
||||
package com.hippo.ehviewer.client.parser
|
||||
|
||||
import com.hippo.ehviewer.client.exception.NotLoggedInException
|
||||
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
|
||||
|
||||
object FavoritesParser {
|
||||
suspend fun parse(body: ByteBuffer): Result {
|
||||
val catArray = arrayOfNulls<String>(10)
|
||||
val countArray = runSuspendCatching {
|
||||
parseFav(body, favCat = catArray).also { check(it.isNotEmpty()) }
|
||||
}.getOrElse {
|
||||
if (it is RuntimeException && it.message == "Not logged in!") throw NotLoggedInException()
|
||||
throw ParseException("Parse favorites error", it)
|
||||
}
|
||||
val result = GalleryListParser.parse(body)
|
||||
return Result(catArray.requireNoNulls(), countArray, result)
|
||||
}
|
||||
|
||||
class Result(
|
||||
val catArray: Array<String>,
|
||||
val countArray: IntArray,
|
||||
galleryListResult: GalleryListResult,
|
||||
) {
|
||||
val prev = galleryListResult.prev
|
||||
val next = galleryListResult.next
|
||||
val galleryInfoList = galleryListResult.galleryInfoList
|
||||
}
|
||||
@Serializable
|
||||
data class FavParserResult(
|
||||
val catArray: List<String>,
|
||||
val countArray: List<Int>,
|
||||
val galleryListResult: GalleryListResult,
|
||||
) {
|
||||
val prev = galleryListResult.prev
|
||||
val next = galleryListResult.next
|
||||
val galleryInfoList = galleryListResult.galleryInfoList
|
||||
}
|
||||
|
||||
private external fun parseFav(body: ByteBuffer, limit: Int = body.limit(), favCat: Array<String?>): IntArray
|
||||
object FavoritesParser {
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -60,6 +60,6 @@ object GalleryApiParser {
|
||||
@SerialName("filecount")
|
||||
val pages: Int,
|
||||
val rating: Float,
|
||||
val tags: ArrayList<String>,
|
||||
val tags: List<String>,
|
||||
)
|
||||
}
|
||||
@@ -38,14 +38,15 @@ import com.hippo.ehviewer.client.exception.ParseException
|
||||
import com.hippo.ehviewer.client.exception.PiningException
|
||||
import com.hippo.ehviewer.client.getThumbKey
|
||||
import com.hippo.ehviewer.util.ExceptionUtils
|
||||
import com.hippo.ehviewer.util.toEpochMillis
|
||||
import com.hippo.ehviewer.util.toFloatOrDefault
|
||||
import com.hippo.ehviewer.util.toIntOrDefault
|
||||
import com.hippo.ehviewer.util.trimAnd
|
||||
import com.hippo.ehviewer.util.unescapeXml
|
||||
import java.time.Instant
|
||||
import java.time.ZoneOffset
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.Locale
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import kotlinx.datetime.LocalDateTime
|
||||
import kotlinx.datetime.format.MonthNames
|
||||
import kotlinx.datetime.format.char
|
||||
import org.jsoup.Jsoup
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
@@ -77,8 +78,19 @@ object GalleryDetailParser {
|
||||
private val PATTERN_FAVORITE_SLOT =
|
||||
Regex("/fav.png\\); background-position:0px -(\\d+)px")
|
||||
private val EMPTY_GALLERY_COMMENT_LIST = GalleryCommentList(emptyList(), false)
|
||||
private val WEB_COMMENT_DATE_FORMAT = DateTimeFormatter
|
||||
.ofPattern("dd MMMM yyyy, HH:mm", Locale.US).withZone(ZoneOffset.UTC)
|
||||
|
||||
// dd MMMM yyyy, HH:mm
|
||||
private val WEB_COMMENT_DATE_FORMAT = LocalDateTime.Format {
|
||||
dayOfMonth()
|
||||
char(' ')
|
||||
monthName(MonthNames.ENGLISH_FULL)
|
||||
char(' ')
|
||||
year()
|
||||
chars(", ")
|
||||
hour()
|
||||
char(':')
|
||||
minute()
|
||||
}
|
||||
private const val OFFENSIVE_STRING =
|
||||
"<p>(And if you choose to ignore this warning, you lose all rights to complain about it in the future.)</p>"
|
||||
private const val PINING_STRING = "<p>This gallery is pining for the fjords.</p>"
|
||||
@@ -93,8 +105,7 @@ object GalleryDetailParser {
|
||||
|
||||
// Error info
|
||||
PATTERN_ERROR.find(body)?.run { throw EhException(groupValues[1]) }
|
||||
// Temporary workaround, see https://github.com/jhy/jsoup/issues/1850
|
||||
val document = Jsoup.parse(body.replace("del>", "s>"))
|
||||
val document = Jsoup.parse(body)
|
||||
val galleryDetail = GalleryDetail(
|
||||
tags = parseTagGroups(document),
|
||||
comments = parseComments(document),
|
||||
@@ -103,8 +114,8 @@ object GalleryDetailParser {
|
||||
)
|
||||
parseDetail(galleryDetail, document, body)
|
||||
|
||||
// Generate simpleLanguage for local favorites
|
||||
galleryDetail.generateSLang()
|
||||
// Fill info for database
|
||||
galleryDetail.fillInfo()
|
||||
return galleryDetail
|
||||
}
|
||||
|
||||
@@ -220,15 +231,10 @@ object GalleryDetailParser {
|
||||
// newer version
|
||||
d.getElementById("gnd")?.run {
|
||||
val dates = PATTERN_NEWER_DATE.findAll(body).map { it.groupValues[1] }.toList()
|
||||
select("a").forEachIndexed { index, element ->
|
||||
val gi = BaseGalleryInfo()
|
||||
gd.newerVersions = select("a").mapIndexedNotNull { index, element ->
|
||||
val result = GalleryDetailUrlParser.parse(element.attr("href"))
|
||||
if (result != null) {
|
||||
gi.gid = result.gid
|
||||
gi.token = result.token
|
||||
gi.title = element.text().trim()
|
||||
gi.posted = dates[index]
|
||||
gd.newerVersions.add(gi)
|
||||
result?.run {
|
||||
BaseGalleryInfo(gid, token, element.text().trim(), posted = dates[index])
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -277,56 +283,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)
|
||||
e.printStackTrace()
|
||||
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)
|
||||
e.printStackTrace()
|
||||
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)
|
||||
e.printStackTrace()
|
||||
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? {
|
||||
@@ -377,7 +377,7 @@ object GalleryDetailParser {
|
||||
} else {
|
||||
temp.substring("Posted on ".length) to null
|
||||
}
|
||||
val time = Instant.from(WEB_COMMENT_DATE_FORMAT.parse(timeStr)).toEpochMilli()
|
||||
val time = WEB_COMMENT_DATE_FORMAT.parse(timeStr).toEpochMillis()
|
||||
// comment
|
||||
val c6 = element.getElementsByClass("c6").first()
|
||||
// fix underline support
|
||||
@@ -396,14 +396,14 @@ object GalleryDetailParser {
|
||||
}
|
||||
// last edited
|
||||
val c8 = element.getElementsByClass("c8").first()
|
||||
val lastEdited = c8?.children()?.first()?.run { Instant.from(WEB_COMMENT_DATE_FORMAT.parse(text())).toEpochMilli() } ?: 0
|
||||
val lastEdited = c8?.children()?.first()?.run { WEB_COMMENT_DATE_FORMAT.parse(text()).toEpochMillis() } ?: 0
|
||||
GalleryComment(
|
||||
id, score, editable, voteUpAble, voteUpEd, voteDownAble,
|
||||
voteDownEd, uploader, voteState, time, user, commentHtml, lastEdited,
|
||||
)
|
||||
} catch (e: Throwable) {
|
||||
ExceptionUtils.throwIfFatal(e)
|
||||
e.printStackTrace()
|
||||
logcat(e)
|
||||
null
|
||||
}
|
||||
}
|
||||
@@ -412,6 +412,8 @@ object GalleryDetailParser {
|
||||
* Parse comments with html parser
|
||||
*/
|
||||
suspend fun parseComments(document: Document): GalleryCommentList {
|
||||
// Disable pretty print to get comments in raw html
|
||||
document.outputSettings().prettyPrint(false)
|
||||
return try {
|
||||
val cdiv = document.getElementById("cdiv")!!
|
||||
val c1s = cdiv.getElementsByClass("c1")
|
||||
@@ -433,7 +435,7 @@ object GalleryDetailParser {
|
||||
GalleryCommentList(list, hasMore)
|
||||
} catch (e: Throwable) {
|
||||
ExceptionUtils.throwIfFatal(e)
|
||||
e.printStackTrace()
|
||||
logcat(e)
|
||||
EMPTY_GALLERY_COMMENT_LIST
|
||||
}
|
||||
}
|
||||
@@ -441,22 +443,16 @@ 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))
|
||||
|
||||
@@ -16,29 +16,23 @@
|
||||
package com.hippo.ehviewer.client.parser
|
||||
|
||||
import com.hippo.ehviewer.client.EhUrl
|
||||
import com.hippo.ehviewer.util.toLongOrDefault
|
||||
import java.util.regex.Pattern
|
||||
|
||||
/**
|
||||
* Like http://exhentai.org/g/1234567/a1b2c3d4e5<br></br>
|
||||
*/
|
||||
object GalleryDetailUrlParser {
|
||||
private val URL_STRICT_PATTERN = Pattern.compile(
|
||||
private val URL_STRICT_PATTERN = Regex(
|
||||
"https?://(?:${EhUrl.DOMAIN_EX}|${EhUrl.DOMAIN_E}(?:/lofi)?)/(?:g|mpv)/(\\d+)/([0-9a-f]{10})",
|
||||
)
|
||||
private val URL_PATTERN = Pattern.compile("(\\d+)/([0-9a-f]{10})(?:[^0-9a-f]|$)")
|
||||
private val URL_PATTERN = Regex("(\\d+)/([0-9a-f]{10})(?:[^0-9a-f]|$)")
|
||||
|
||||
fun parse(url: String?, strict: Boolean = true): Result? {
|
||||
url ?: return null
|
||||
val pattern = if (strict) URL_STRICT_PATTERN else URL_PATTERN
|
||||
val m = pattern.matcher(url)
|
||||
return if (m.find()) {
|
||||
val gid = m.group(1)!!.toLongOrDefault(0).takeIf { it > 0 }
|
||||
gid?.let { Result(it, m.group(2)!!) }
|
||||
} else {
|
||||
null
|
||||
return pattern.find(url)?.destructured?.let { (gid, token) ->
|
||||
Result(gid.toLong(), token)
|
||||
}
|
||||
}
|
||||
|
||||
class Result(val gid: Long, val token: String)
|
||||
data class Result(val gid: Long, val token: String)
|
||||
}
|
||||
@@ -1,48 +1,26 @@
|
||||
package com.hippo.ehviewer.client.parser
|
||||
|
||||
import androidx.annotation.Keep
|
||||
import com.hippo.ehviewer.EhDB
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.client.data.BaseGalleryInfo
|
||||
import com.hippo.ehviewer.client.data.GalleryInfo.Companion.LOCAL_FAVORITED
|
||||
import com.hippo.ehviewer.client.data.GalleryInfo.Companion.NOT_FAVORITED
|
||||
import com.hippo.ehviewer.client.exception.EhException
|
||||
import com.hippo.ehviewer.client.exception.ParseException
|
||||
import java.nio.ByteBuffer
|
||||
import kotlinx.serialization.Serializable
|
||||
import moe.tarsin.coroutines.runSuspendCatching
|
||||
|
||||
object GalleryListParser {
|
||||
suspend fun parse(body: ByteBuffer) = runSuspendCatching {
|
||||
parseGalleryInfoList(body)
|
||||
}.onFailure {
|
||||
if (it is RuntimeException) {
|
||||
if (it.message?.startsWith("Your IP address") == true) {
|
||||
throw EhException(it.message)
|
||||
}
|
||||
if (it.message == "No watched tags!") {
|
||||
throw EhException(R.string.gallery_list_empty_hit_subscription)
|
||||
}
|
||||
if (it.message == "No hits found!") {
|
||||
throw EhException(R.string.gallery_list_empty_hit)
|
||||
}
|
||||
}
|
||||
fun parse(body: ByteBuffer) = runSuspendCatching {
|
||||
unmarshalParsingAs<GalleryListResult>(body, ::parseGalleryInfoList)
|
||||
}.getOrElse {
|
||||
throw ParseException("Can't parse gallery list", it)
|
||||
}.getOrThrow().apply {
|
||||
galleryInfoList.onEach {
|
||||
if (it.favoriteSlot == NOT_FAVORITED && EhDB.containLocalFavorites(it.gid)) {
|
||||
it.favoriteSlot = LOCAL_FAVORITED
|
||||
}
|
||||
it.generateSLang()
|
||||
}
|
||||
}
|
||||
|
||||
val emptyResult = GalleryListResult(null, null, arrayListOf())
|
||||
}
|
||||
|
||||
class GalleryListResult @Keep constructor(
|
||||
@Serializable
|
||||
data class GalleryListResult(
|
||||
val prev: String?,
|
||||
val next: String?,
|
||||
val galleryInfoList: ArrayList<BaseGalleryInfo>,
|
||||
)
|
||||
|
||||
private external fun parseGalleryInfoList(body: ByteBuffer, size: Int = body.limit()): GalleryListResult
|
||||
private external fun parseGalleryInfoList(body: ByteBuffer, size: Int = body.limit()): Int
|
||||
@@ -19,6 +19,7 @@ import android.net.Uri
|
||||
import android.os.Build
|
||||
import com.hippo.ehviewer.client.EhUrl
|
||||
import com.hippo.ehviewer.client.data.ListUrlBuilder
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import java.io.UnsupportedEncodingException
|
||||
import java.net.MalformedURLException
|
||||
import java.net.URL
|
||||
@@ -77,7 +78,7 @@ object GalleryListUrlParser {
|
||||
try {
|
||||
URLDecoder.decode(uploader, StandardCharsets.UTF_8.displayName())
|
||||
} catch (e: UnsupportedEncodingException) {
|
||||
e.printStackTrace()
|
||||
logcat(e)
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -106,7 +107,7 @@ object GalleryListUrlParser {
|
||||
try {
|
||||
URLDecoder.decode(tag, StandardCharsets.UTF_8.displayName())
|
||||
} catch (e: UnsupportedEncodingException) {
|
||||
e.printStackTrace()
|
||||
logcat(e)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,17 +18,16 @@
|
||||
package com.hippo.ehviewer.client.parser
|
||||
|
||||
import com.hippo.ehviewer.util.ExceptionUtils
|
||||
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)
|
||||
it.printStackTrace()
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,7 @@ import com.hippo.ehviewer.util.unescapeXml
|
||||
|
||||
object GalleryPageParser {
|
||||
private val PATTERN_IMAGE_URL = Regex("<img[^>]*src=\"([^\"]+)\" style")
|
||||
private val PATTERN_SKIP_HATH_KEY = Regex("onclick=\"return nl\\('([\\d-]+)'\\)")
|
||||
private val PATTERN_SKIP_HATH_KEY = Regex("onclick=\"return nl\\('([^)]+)'\\)")
|
||||
private val PATTERN_ORIGIN_IMAGE_URL = Regex("<a href=\"([^\"]+/fullimg/[^\"]+)\">")
|
||||
|
||||
// TODO Not sure about the size of show keys
|
||||
|
||||
@@ -16,34 +16,21 @@
|
||||
package com.hippo.ehviewer.client.parser
|
||||
|
||||
import com.hippo.ehviewer.client.EhUrl
|
||||
import com.hippo.ehviewer.util.toIntOrDefault
|
||||
import com.hippo.ehviewer.util.toLongOrDefault
|
||||
import java.util.regex.Pattern
|
||||
|
||||
/**
|
||||
* Like http://exhentai.org/s/91ea4b6d89/901103-12
|
||||
*/
|
||||
object GalleryPageUrlParser {
|
||||
private val URL_STRICT_PATTERN = Pattern.compile(
|
||||
private val URL_STRICT_PATTERN = Regex(
|
||||
"https?://(?:${EhUrl.DOMAIN_EX}|${EhUrl.DOMAIN_E}(?:/lofi)?)/s/([0-9a-f]{10})/(\\d+)-(\\d+)",
|
||||
)
|
||||
private val URL_PATTERN = Pattern.compile("([0-9a-f]{10})/(\\d+)-(\\d+)")
|
||||
private val URL_PATTERN = Regex("([0-9a-f]{10})/(\\d+)-(\\d+)")
|
||||
|
||||
fun parse(url: String?, strict: Boolean = true): Result? {
|
||||
url ?: return null
|
||||
val pattern = if (strict) URL_STRICT_PATTERN else URL_PATTERN
|
||||
val m = pattern.matcher(url)
|
||||
return if (m.find()) {
|
||||
val gid = m.group(2)!!.toLongOrDefault(-1L)
|
||||
val pToken = m.group(1)!!
|
||||
val page = m.group(3)!!.toIntOrDefault(0) - 1
|
||||
if (gid < 0 || page < 0) {
|
||||
null
|
||||
} else {
|
||||
Result(gid, pToken, page)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
return pattern.find(url)?.destructured?.let { (pToken, gid, page) ->
|
||||
Result(gid.toLong(), pToken, page.toInt() - 1)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
package com.hippo.ehviewer.client.parser
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.annotation.Keep
|
||||
import com.hippo.ehviewer.client.exception.InsufficientFundsException
|
||||
import com.hippo.ehviewer.client.exception.ParseException
|
||||
import java.nio.ByteBuffer
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
object HomeParser {
|
||||
private val PATTERN_FUNDS = Regex("Available: ([\\d,]+) Credits.*Available: ([\\d,]+) kGP", RegexOption.DOT_MATCHES_ALL)
|
||||
private const val INSUFFICIENT_FUNDS = "Insufficient funds."
|
||||
|
||||
fun parse(body: ByteBuffer) = runCatching {
|
||||
parseLimit(body)
|
||||
unmarshalParsingAs<Limits>(body, ::parseLimit)
|
||||
}.getOrElse { throw ParseException("Parse image limits error", it) }
|
||||
|
||||
fun parseResetLimits(body: String) {
|
||||
@@ -25,17 +25,20 @@ 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
|
||||
class Result(val limits: Limits, val funds: Funds)
|
||||
data class Result(val limits: Limits, val funds: Funds) : Parcelable
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
data class Limits @Keep constructor(val current: Int, val maximum: Int, val resetCost: Int) : Parcelable
|
||||
data class Funds(val gp: String, val credit: String) : Parcelable
|
||||
|
||||
private external fun parseLimit(body: ByteBuffer, limit: Int = body.limit()): Limits
|
||||
@Parcelize
|
||||
@Serializable
|
||||
data class Limits(val current: Int, val maximum: Int, val resetCost: Int) : Parcelable
|
||||
|
||||
private external fun parseLimit(body: ByteBuffer, limit: Int = body.limit()): Int
|
||||
@@ -15,32 +15,35 @@
|
||||
*/
|
||||
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
|
||||
import com.hippo.ehviewer.util.unescapeXml
|
||||
import java.time.Instant
|
||||
import java.time.ZoneOffset
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.Locale
|
||||
import kotlinx.datetime.LocalDateTime
|
||||
import kotlinx.datetime.format.char
|
||||
|
||||
object ParserUtils {
|
||||
private val formatter =
|
||||
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm", Locale.US).withZone(ZoneOffset.UTC)
|
||||
|
||||
@Synchronized
|
||||
fun formatDate(time: Long): String {
|
||||
return formatter.format(Instant.ofEpochMilli(time))
|
||||
// yyyy-MM-dd HH:mm
|
||||
private val formatter = LocalDateTime.Format {
|
||||
year()
|
||||
char('-')
|
||||
monthNumber()
|
||||
char('-')
|
||||
dayOfMonth()
|
||||
char(' ')
|
||||
hour()
|
||||
char(':')
|
||||
minute()
|
||||
}
|
||||
|
||||
fun trim(str: String?): String {
|
||||
return str?.unescapeXml()?.trim() ?: ""
|
||||
}
|
||||
fun formatDate(time: Long): String = formatter.format(time.toLocalDateTime())
|
||||
|
||||
fun parseInt(str: String?, defValue: Int): Int {
|
||||
return trim(str).replace(",", "").toIntOrDefault(defValue)
|
||||
}
|
||||
fun parseDate(str: String): Long = formatter.parse(str).toEpochMillis()
|
||||
|
||||
fun parseLong(str: String?, defValue: Long): Long {
|
||||
return trim(str).replace(",", "").toLongOrDefault(defValue)
|
||||
}
|
||||
fun trim(str: String?): String = str?.unescapeXml()?.trim() ?: ""
|
||||
|
||||
fun parseInt(str: String?, defValue: Int): Int = trim(str).replace(",", "").toIntOrDefault(defValue)
|
||||
|
||||
fun parseLong(str: String?, defValue: Long): Long = trim(str).replace(",", "").toLongOrDefault(defValue)
|
||||
}
|
||||
@@ -15,39 +15,36 @@
|
||||
*/
|
||||
package com.hippo.ehviewer.client.parser
|
||||
|
||||
import android.util.Log
|
||||
import com.hippo.ehviewer.client.EhUrl
|
||||
import com.hippo.ehviewer.client.exception.ParseException
|
||||
import com.hippo.ehviewer.util.ExceptionUtils
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import org.jsoup.Jsoup
|
||||
|
||||
object ProfileParser {
|
||||
private val TAG = ProfileParser::class.java.simpleName
|
||||
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.isNullOrEmpty()) {
|
||||
null
|
||||
} else if (!avatar.startsWith("http")) {
|
||||
EhUrl.URL_FORUMS + avatar
|
||||
} else {
|
||||
avatar
|
||||
}
|
||||
}.getOrElse {
|
||||
ExceptionUtils.throwIfFatal(it)
|
||||
Log.i(TAG, "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?)
|
||||
|
||||
@@ -17,26 +17,17 @@ package com.hippo.ehviewer.client.parser
|
||||
|
||||
import com.hippo.ehviewer.client.exception.EhException
|
||||
import com.hippo.ehviewer.client.exception.ParseException
|
||||
import java.util.regex.Pattern
|
||||
|
||||
object SignInParser {
|
||||
private val NAME_PATTERN = Pattern.compile("<p>You are now logged in as: (.+?)<")
|
||||
private val ERROR_PATTERN = Pattern.compile(
|
||||
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 {
|
||||
var m = NAME_PATTERN.matcher(body)
|
||||
return if (m.find()) {
|
||||
m.group(1)!!
|
||||
} else {
|
||||
m = ERROR_PATTERN.matcher(body)
|
||||
if (m.find()) {
|
||||
throw EhException(m.group(1) ?: m.group(2))
|
||||
} else {
|
||||
throw ParseException("Can't parse sign in")
|
||||
}
|
||||
}
|
||||
}
|
||||
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")
|
||||
}
|
||||
@@ -1,27 +1,32 @@
|
||||
package com.hippo.ehviewer.client.parser
|
||||
|
||||
import androidx.annotation.Keep
|
||||
import com.hippo.ehviewer.client.exception.ParseException
|
||||
import java.nio.ByteBuffer
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
object TorrentParser {
|
||||
fun parse(body: ByteBuffer) = runCatching {
|
||||
parseTorrent(body)
|
||||
}.getOrElse { throw ParseException("Can't parse torrent list", it) }
|
||||
unmarshalParsingAs<TorrentResult>(body, ::parseTorrent)
|
||||
}.getOrElse {
|
||||
throw ParseException("Can't parse torrent list", it)
|
||||
}
|
||||
}
|
||||
|
||||
class Torrent @Keep constructor(
|
||||
@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>
|
||||
|
||||
private external fun parseTorrent(body: ByteBuffer, size: Int = body.limit()): ArrayList<Torrent>
|
||||
private external fun parseTorrent(body: ByteBuffer, size: Int = body.limit()): Int
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Loaded 100 of 472 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user