Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
37eb2f1aed | ||
|
|
c70ec5f354 | ||
|
|
7139af4629 | ||
|
|
a2bac029f5 | ||
|
|
6c68074e8b | ||
|
|
991caf73f4 | ||
|
|
0ba11641fa | ||
|
|
b51120e914 | ||
|
|
b02fb2d1d7 | ||
|
|
c6247230da | ||
|
|
d64be78b4e | ||
|
|
e75f633165 | ||
|
|
27c5598133 | ||
|
|
a9449bfbc7 | ||
|
|
4fecbe259b | ||
|
|
81d72edbf0 | ||
|
|
ea7e6e4624 | ||
|
|
6335d5bca4 | ||
|
|
227b430dbb | ||
|
|
0bba56a29f | ||
|
|
c07d9da7b0 | ||
|
|
900c5ed007 | ||
|
|
ba0788f6df | ||
|
|
c517856e89 | ||
|
|
791ee0ddbf | ||
|
|
0746938cb9 | ||
|
|
bbae24aba9 | ||
|
|
677edb0e02 | ||
|
|
a4328c5111 | ||
|
|
27cf341fef | ||
|
|
bc654e98b8 | ||
|
|
588f78e598 | ||
|
|
70f952285a | ||
|
|
a6f5e4f98d | ||
|
|
6fe5c1d666 | ||
|
|
1dba12ec94 | ||
|
|
aeb3f689d1 | ||
|
|
0a2268d4de | ||
|
|
9a32b020cf | ||
|
|
c5b01a7089 | ||
|
|
0634fbb724 | ||
|
|
445e9511a0 | ||
|
|
ac532acb5b | ||
|
|
7a8e21fbef | ||
|
|
58b0f3a41e | ||
|
|
ab373990cd | ||
|
|
272c157fe0 | ||
|
|
e5e047ee10 | ||
|
|
bf0b529018 | ||
|
|
acf2392406 | ||
|
|
cecd6fb359 | ||
|
|
7b67210876 | ||
|
|
e40c4f2c71 | ||
|
|
b0717b38b1 | ||
|
|
18c9b95b9b | ||
|
|
fc63c41ab0 | ||
|
|
d1c7434993 | ||
|
|
da15ba8b9a | ||
|
|
1d2a2a3608 | ||
|
|
145cd7a7c0 | ||
|
|
048415f3e0 | ||
|
|
95ccabc756 | ||
|
|
1f08c0e08c | ||
|
|
b036aeef23 | ||
|
|
d469a8404c | ||
|
|
5b16b6aed9 | ||
|
|
151949aecf | ||
|
|
de2c4ca3f5 | ||
|
|
e5bebcfe80 | ||
|
|
0080a482e4 | ||
|
|
756867416b | ||
|
|
124ff2f190 | ||
|
|
828a3d09b6 | ||
|
|
fc3b9d73ec | ||
|
|
ba943a31d4 | ||
|
|
8ee1ac7617 | ||
|
|
113f2a1794 | ||
|
|
f6028813e2 | ||
|
|
b094a32d12 | ||
|
|
fd3e4bdc60 | ||
|
|
142092d930 | ||
|
|
75e3e78ecb | ||
|
|
053832681f | ||
|
|
29e33d6e69 | ||
|
|
2d878e2f1c | ||
|
|
0fdbfbb1ae | ||
|
|
52f314f6b0 | ||
|
|
ff8d8916aa | ||
|
|
b2368b312f | ||
|
|
3cade906d6 | ||
|
|
e7cb33ef6f | ||
|
|
8c331c9577 | ||
|
|
8560a050ba | ||
|
|
583637b692 | ||
|
|
791359e949 | ||
|
|
e238b6aec0 | ||
|
|
17a143c4de | ||
|
|
dfb0aa823e | ||
|
|
b5cad36967 | ||
|
|
86378630bb | ||
|
|
730035aff7 | ||
|
|
64ac770c56 | ||
|
|
557a5599e2 | ||
|
|
ffd24015ac | ||
|
|
5eb1f12113 | ||
|
|
8425fed5a7 | ||
|
|
1e34aa4d9a | ||
|
|
f383594050 | ||
|
|
086db7572d | ||
|
|
082e05584a | ||
|
|
fb20f899e2 | ||
|
|
c0c07ff2cf | ||
|
|
2f67b741ff | ||
|
|
5a96048051 | ||
|
|
5d8b38ecf9 | ||
|
|
2941ac8285 | ||
|
|
07e316d9a9 | ||
|
|
5bd9fa3a62 | ||
|
|
0b5da2b2dd | ||
|
|
714a8ccf65 | ||
|
|
5c731cc6b2 | ||
|
|
eda8115fe5 | ||
|
|
cf0a408b90 | ||
|
|
f9786377a5 | ||
|
|
93f234b16d | ||
|
|
afb986937b | ||
|
|
61fecaffe1 | ||
|
|
6f87afc1bf | ||
|
|
2e5950a1a8 | ||
|
|
4bf0c4ecec | ||
|
|
3a8dbcdb32 | ||
|
|
1f3546f3cc | ||
|
|
f864d65d98 | ||
|
|
3479498c6c | ||
|
|
ac319330be | ||
|
|
9b3595e45f | ||
|
|
d855858543 | ||
|
|
ada52f1687 | ||
|
|
ef68374f52 | ||
|
|
0bf8049021 | ||
|
|
3cff3f837a | ||
|
|
ca94a63762 | ||
|
|
81b8bc9029 | ||
|
|
9d38520b85 | ||
|
|
800e09adad | ||
|
|
30d6c11220 | ||
|
|
e465356df8 | ||
|
|
ff8e1b7d28 | ||
|
|
175e1d7df9 | ||
|
|
271c1b1390 | ||
|
|
a128356fc4 | ||
|
|
2ed957b5f2 | ||
|
|
85bb9b9f97 | ||
|
|
050b5c0451 | ||
|
|
a14df3fc0c | ||
|
|
939136476e | ||
|
|
7e2af653ee | ||
|
|
d357a55307 | ||
|
|
23509ac2c9 | ||
|
|
118a4f75c0 | ||
|
|
2b8a7fafe6 | ||
|
|
10eda7e5f3 | ||
|
|
c23cadec8a | ||
|
|
0114081a2b | ||
|
|
2611601fb9 | ||
|
|
4194087f0b | ||
|
|
673adc41ba | ||
|
|
b195ae25a8 | ||
|
|
556c49fbfd | ||
|
|
358a5249a4 | ||
|
|
d5b837e6af | ||
|
|
61100d0c83 | ||
|
|
8473ca9fff | ||
|
|
37a2227c5c | ||
|
|
d6a4ab7471 | ||
|
|
6291288c42 | ||
|
|
6c926419e9 | ||
|
|
f292f31ac1 | ||
|
|
86a49f764e | ||
|
|
5dad21f4ea | ||
|
|
a4e9291bf9 | ||
|
|
2a667e62f4 | ||
|
|
6bd8063976 | ||
|
|
0aba5907b0 | ||
|
|
32182a7791 | ||
|
|
198960f93c | ||
|
|
618f14a732 | ||
|
|
fec2c39364 | ||
|
|
dd06d82cd9 | ||
|
|
4c98ee5350 | ||
|
|
2800a7bc15 | ||
|
|
04818e5afb | ||
|
|
122089eb1e | ||
|
|
b991e5d70a | ||
|
|
0d00ae207a | ||
|
|
9e451c18d9 | ||
|
|
71ebd99115 | ||
|
|
116dbead4b | ||
|
|
ef8adb00f4 | ||
|
|
7d74781f96 | ||
|
|
b7627dc576 | ||
|
|
cf42acd65c | ||
|
|
4163c895df | ||
|
|
63ddd5ea89 | ||
|
|
216dfded4b | ||
|
|
08b5eac6bd | ||
|
|
6e541be6ca | ||
|
|
17a9c02f96 | ||
|
|
ed640ab00e | ||
|
|
d69df5f7ab | ||
|
|
6f6461e855 | ||
|
|
682d959688 | ||
|
|
179686de16 | ||
|
|
32e35622bd | ||
|
|
c9c06202bb | ||
|
|
42d17621f9 | ||
|
|
629044c241 | ||
|
|
9be19489a4 | ||
|
|
cde4f9e405 | ||
|
|
16fddcb377 | ||
|
|
35cff4647a | ||
|
|
80ad749fc2 | ||
|
|
1a1d444b20 | ||
|
|
bb2d500067 | ||
|
|
c3c7e88c15 | ||
|
|
8dc3b1f376 | ||
|
|
b49a80bb35 | ||
|
|
21bfd4dd2d | ||
|
|
3551ed7c7a | ||
|
|
5f8ab98e9e | ||
|
|
8c6b2f5366 | ||
|
|
bac60fbec8 | ||
|
|
4eed03ebed | ||
|
|
e33295d0a6 | ||
|
|
07a495c499 | ||
|
|
e69eba6023 | ||
|
|
8be6ad84fc | ||
|
|
59e46aeeea | ||
|
|
4404531171 | ||
|
|
99db84fdd0 | ||
|
|
aa50b69754 | ||
|
|
fa2933df7c | ||
|
|
c51a26b9b7 | ||
|
|
9859a1eaac | ||
|
|
cd3858ace8 | ||
|
|
0c54e8b0b3 | ||
|
|
e32a7b61b6 | ||
|
|
ea2977268b | ||
|
|
6b529bcb18 | ||
|
|
941e7d18f3 | ||
|
|
d0fe9fac8c | ||
|
|
8f37c5ba25 | ||
|
|
b63b36e6d2 | ||
|
|
081357cbb4 | ||
|
|
0f7aa5f9ea | ||
|
|
a565b428e8 | ||
|
|
71f4df6f05 | ||
|
|
5da2f91e6a | ||
|
|
a6e9867596 | ||
|
|
9e1cff014d | ||
|
|
9c06f10d43 | ||
|
|
3f11df1dc6 | ||
|
|
6dd4505cec | ||
|
|
8faabd7759 | ||
|
|
288e56d7e0 | ||
|
|
7bcddb5f51 | ||
|
|
7b0bd309e4 | ||
|
|
237938b1f0 | ||
|
|
07009f8bd6 | ||
|
|
f7ef768c8f | ||
|
|
d1c3c9a37a | ||
|
|
78f15860e0 | ||
|
|
6b11773da9 | ||
|
|
ed26cb8f35 | ||
|
|
337f27dc38 | ||
|
|
97fab45877 | ||
|
|
8e8749ed6e | ||
|
|
a0e999576e | ||
|
|
b29802c260 | ||
|
|
665e5a3d39 | ||
|
|
c2a2dafb47 | ||
|
|
1d12a16e3d | ||
|
|
f3fe5f0a75 | ||
|
|
3bad1d06f4 | ||
|
|
70f2bfb760 | ||
|
|
9e86d42972 | ||
|
|
c8ade4d750 | ||
|
|
11b7bbd7ef | ||
|
|
513bce51ea | ||
|
|
f2c8c5d66d | ||
|
|
4e08f4f962 | ||
|
|
a9bd55dff5 | ||
|
|
5cec825dbe | ||
|
|
14c5b28cdc | ||
|
|
7693b11e0a | ||
|
|
f9cb49e4d3 | ||
|
|
892a614246 | ||
|
|
4a482d996e | ||
|
|
af9baa8ce4 | ||
|
|
39a11525a9 | ||
|
|
2337dda4eb | ||
|
|
c0a610a02b | ||
|
|
85f7b54719 | ||
|
|
e0aea2fedf | ||
|
|
ee83dbb877 | ||
|
|
1de4bc11eb | ||
|
|
04a7656afa | ||
|
|
77ec7e76e9 | ||
|
|
9ca7be081a | ||
|
|
fb36346f0b | ||
|
|
95679f3fb9 | ||
|
|
addd4124c1 | ||
|
|
74b79d9faa | ||
|
|
9daa05ef02 | ||
|
|
5ee884e0a2 | ||
|
|
dba7f4e6c0 | ||
|
|
8221514805 | ||
|
|
2ae3effc0e | ||
|
|
5b450ff18f | ||
|
|
92be6e9340 | ||
|
|
8884aeec11 | ||
|
|
8e15eda4e3 | ||
|
|
0dbf220655 | ||
|
|
fe69d59ef1 | ||
|
|
1e1f8350b5 | ||
|
|
afa2d03b56 | ||
|
|
1293d7fe28 | ||
|
|
62e879f2de | ||
|
|
a771994f6f | ||
|
|
8c2ba051a0 | ||
|
|
ae2f289122 | ||
|
|
703d63d106 | ||
|
|
2d7b024293 | ||
|
|
26906f6709 | ||
|
|
e9ec97403f | ||
|
|
34fb72707b | ||
|
|
fb7eb64c3a | ||
|
|
ef65720999 | ||
|
|
3bc71d6798 | ||
|
|
c4be269fb7 | ||
|
|
1238edfe2b | ||
|
|
747fd23b96 | ||
|
|
022f7a3ef1 | ||
|
|
e0cad8600c | ||
|
|
9510ebbc31 | ||
|
|
b63f2fd393 | ||
|
|
95a0e9f03b | ||
|
|
8265dd7f02 | ||
|
|
67feb19cab | ||
|
|
614e847de0 | ||
|
|
c2ff4b6a0b | ||
|
|
b273f1039c | ||
|
|
bea0c6a1f0 | ||
|
|
dcfaa361ec | ||
|
|
9904e166d2 | ||
|
|
5f02769012 | ||
|
|
19e5636dd2 | ||
|
|
9a220f89d9 | ||
|
|
1c76ec813c | ||
|
|
d36c189430 | ||
|
|
61e2ace5c8 | ||
|
|
d6a3f21754 | ||
|
|
4e54d7f66f | ||
|
|
f2dff038cd | ||
|
|
a7b30640e0 | ||
|
|
45048b1f46 | ||
|
|
498129314a | ||
|
|
3ee4b4a61e | ||
|
|
ee59c0f22e | ||
|
|
c56134cd92 | ||
|
|
b3d1b66a06 | ||
|
|
c9bb81bf63 | ||
|
|
d176810dae | ||
|
|
17bdca71b1 | ||
|
|
22fd3730df | ||
|
|
23d1271949 |
No files matched your search
@@ -141,9 +141,6 @@ body:
|
|||||||
请确认您已经遵守所有必选项
|
请确认您已经遵守所有必选项
|
||||||
Please ensure you have obtained all needed options
|
Please ensure you have obtained all needed options
|
||||||
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
|
- 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
|
required: true
|
||||||
|
|
||||||
|
|||||||
+120
-146
@@ -4,33 +4,30 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- '*'
|
- '*'
|
||||||
- 'renovate/**'
|
|
||||||
pull_request:
|
pull_request:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
linux:
|
check:
|
||||||
name: Build
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
uses: actions/checkout@v4
|
||||||
with:
|
|
||||||
submodules: 'recursive'
|
|
||||||
|
|
||||||
- name: Setup Java 17
|
- name: Setup Java
|
||||||
uses: actions/setup-java@v3
|
uses: actions/setup-java@v4
|
||||||
with:
|
with:
|
||||||
distribution: temurin
|
distribution: temurin
|
||||||
java-version: 17
|
java-version: 21
|
||||||
|
|
||||||
- name: Install Rust Toolchain
|
- 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
|
- name: Rust Cache
|
||||||
uses: Swatinem/rust-cache@v2
|
uses: Swatinem/rust-cache@v2
|
||||||
with:
|
with:
|
||||||
workspaces: "./app/src/main/rust"
|
workspaces: "./app/src/main/rust"
|
||||||
|
shared-key: ${{ runner.os }}
|
||||||
|
|
||||||
- name: Rustfmt Check
|
- name: Rustfmt Check
|
||||||
working-directory: ./app/src/main/rust
|
working-directory: ./app/src/main/rust
|
||||||
@@ -41,177 +38,154 @@ jobs:
|
|||||||
run: cargo clippy --all-targets --all-features -- -D warnings
|
run: cargo clippy --all-targets --all-features -- -D warnings
|
||||||
|
|
||||||
- name: Setup Gradle
|
- name: Setup Gradle
|
||||||
uses: gradle/gradle-build-action@v2
|
uses: gradle/actions/setup-gradle@v3
|
||||||
with:
|
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.11718014"
|
||||||
|
|
||||||
- name: Spotless Check
|
- name: Spotless Check
|
||||||
run: ./gradlew spotlessCheck
|
run: ./gradlew spotlessCheck
|
||||||
|
|
||||||
- name: Lint Check
|
default:
|
||||||
run: ./gradlew lintMarshmallowGmsRelease
|
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.11718014"
|
||||||
|
|
||||||
|
- 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
|
- name: CMake Cache
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: app/.cxx
|
||||||
app/.cxx
|
key: cmake-default-${{ hashFiles('gradle/libs.versions.toml', 'app/build.gradle.kts', 'app/src/main/cpp/CMakeLists.txt') }}
|
||||||
app/src/main/cpp/nettle/nettle
|
restore-keys: cmake-default-
|
||||||
key: ${{ runner.os }}-cmake-${{ hashFiles('gradle/libs.versions.toml', 'app/build.gradle.kts', 'app/src/main/cpp/CMakeLists.txt') }}
|
|
||||||
restore-keys: ${{ runner.os }}-cmake-
|
|
||||||
|
|
||||||
- name: Build OSS
|
- name: Build
|
||||||
run: ./gradlew assembleDefaultOssRelease
|
run: ./gradlew assembleDefaultRelease
|
||||||
|
|
||||||
- name: Upload OSS ARM64
|
- name: Upload ARM64
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: default-oss-arm64-v8a-${{ github.sha }}
|
name: default-arm64-v8a-${{ github.sha }}
|
||||||
path: app/build/outputs/apk/defaultOss/release/app-default-oss-arm64-v8a-release.apk
|
path: app/build/outputs/apk/default/release/app-default-arm64-v8a-release.apk
|
||||||
|
|
||||||
- name: Upload OSS x86_64
|
- name: Upload x86_64
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: default-oss-x86_64-${{ github.sha }}
|
name: default-x86_64-${{ github.sha }}
|
||||||
path: app/build/outputs/apk/defaultOss/release/app-default-oss-x86_64-release.apk
|
path: app/build/outputs/apk/default/release/app-default-x86_64-release.apk
|
||||||
|
|
||||||
- name: Upload OSS ARM32
|
- name: Upload ARM32
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: default-oss-armeabi-v7a-${{ github.sha }}
|
name: default-armeabi-v7a-${{ github.sha }}
|
||||||
path: app/build/outputs/apk/defaultOss/release/app-default-oss-armeabi-v7a-release.apk
|
path: app/build/outputs/apk/default/release/app-default-armeabi-v7a-release.apk
|
||||||
|
|
||||||
- name: Upload OSS x86
|
- name: Upload mapping
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: default-oss-x86-${{ github.sha }}
|
name: default-mapping-${{ github.sha }}
|
||||||
path: app/build/outputs/apk/defaultOss/release/app-default-oss-x86-release.apk
|
path: app/build/outputs/mapping/defaultRelease/mapping.txt
|
||||||
|
|
||||||
- name: Upload OSS mapping
|
- name: Upload native debug symbols
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: default-oss-mapping-${{ github.sha }}
|
name: default-native-debug-symbols-${{ github.sha }}
|
||||||
path: app/build/outputs/mapping/defaultOssRelease/mapping.txt
|
path: app/build/outputs/native-debug-symbols/defaultRelease/native-debug-symbols.zip
|
||||||
|
|
||||||
- name: Upload OSS native debug symbols
|
marshmallow:
|
||||||
uses: actions/upload-artifact@v3
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Java
|
||||||
|
uses: actions/setup-java@v4
|
||||||
with:
|
with:
|
||||||
name: default-oss-native-debug-symbols-${{ github.sha }}
|
distribution: temurin
|
||||||
path: app/build/outputs/native-debug-symbols/defaultOssRelease/native-debug-symbols.zip
|
java-version: 21
|
||||||
|
|
||||||
- name: Build GMS
|
- name: Install NDK
|
||||||
run: ./gradlew assembleDefaultGmsRelease
|
run: echo "y" | ${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager --install "ndk;27.0.11718014"
|
||||||
|
|
||||||
- name: Upload GMS ARM64
|
- name: Install Rust Toolchain
|
||||||
uses: actions/upload-artifact@v3
|
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:
|
with:
|
||||||
name: default-gms-arm64-v8a-${{ github.sha }}
|
workspaces: "./app/src/main/rust"
|
||||||
path: app/build/outputs/apk/defaultGms/release/app-default-gms-arm64-v8a-release.apk
|
shared-key: ${{ runner.os }}
|
||||||
|
save-if: "false"
|
||||||
|
|
||||||
- name: Upload GMS x86_64
|
- name: Setup Gradle
|
||||||
uses: actions/upload-artifact@v3
|
uses: gradle/actions/setup-gradle@v3
|
||||||
with:
|
with:
|
||||||
name: default-gms-x86_64-${{ github.sha }}
|
cache-encryption-key: ${{ secrets.GradleEncryptionKey }}
|
||||||
path: app/build/outputs/apk/defaultGms/release/app-default-gms-x86_64-release.apk
|
gradle-home-cache-cleanup: true
|
||||||
|
|
||||||
- name: Upload GMS ARM32
|
- name: CMake Cache
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
name: default-gms-armeabi-v7a-${{ github.sha }}
|
path: app/.cxx
|
||||||
path: app/build/outputs/apk/defaultGms/release/app-default-gms-armeabi-v7a-release.apk
|
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
|
- name: Build
|
||||||
uses: actions/upload-artifact@v3
|
run: ./gradlew assembleMarshmallowRelease
|
||||||
|
|
||||||
|
- name: Upload ARM64
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: default-gms-x86-${{ github.sha }}
|
name: marshmallow-arm64-v8a-${{ github.sha }}
|
||||||
path: app/build/outputs/apk/defaultGms/release/app-default-gms-x86-release.apk
|
path: app/build/outputs/apk/marshmallow/release/app-marshmallow-arm64-v8a-release.apk
|
||||||
|
|
||||||
- name: Upload GMS mapping
|
- name: Upload x86_64
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: default-gms-mapping-${{ github.sha }}
|
name: marshmallow-x86_64-${{ github.sha }}
|
||||||
path: app/build/outputs/mapping/defaultGmsRelease/mapping.txt
|
path: app/build/outputs/apk/marshmallow/release/app-marshmallow-x86_64-release.apk
|
||||||
|
|
||||||
- name: Upload GMS native debug symbols
|
- name: Upload ARM32
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: default-gms-native-debug-symbols-${{ github.sha }}
|
name: marshmallow-armeabi-v7a-${{ github.sha }}
|
||||||
path: app/build/outputs/native-debug-symbols/defaultGmsRelease/native-debug-symbols.zip
|
path: app/build/outputs/apk/marshmallow/release/app-marshmallow-armeabi-v7a-release.apk
|
||||||
|
|
||||||
- name: Build Marshmallow OSS
|
- name: Upload mapping
|
||||||
run: ./gradlew assembleMarshmallowOssRelease
|
uses: actions/upload-artifact@v4
|
||||||
|
|
||||||
- name: Upload Marshmallow OSS ARM64
|
|
||||||
uses: actions/upload-artifact@v3
|
|
||||||
with:
|
with:
|
||||||
name: marshmallow-oss-arm64-v8a-${{ github.sha }}
|
name: marshmallow-mapping-${{ github.sha }}
|
||||||
path: app/build/outputs/apk/marshmallowOss/release/app-marshmallow-oss-arm64-v8a-release.apk
|
path: app/build/outputs/mapping/marshmallowRelease/mapping.txt
|
||||||
|
|
||||||
- name: Upload Marshmallow OSS x86_64
|
- name: Upload native debug symbols
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: marshmallow-oss-x86_64-${{ github.sha }}
|
name: marshmallow-native-debug-symbols-${{ github.sha }}
|
||||||
path: app/build/outputs/apk/marshmallowOss/release/app-marshmallow-oss-x86_64-release.apk
|
path: app/build/outputs/native-debug-symbols/marshmallowRelease/native-debug-symbols.zip
|
||||||
|
|
||||||
- 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
|
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
name: 'Lock Threads'
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: '0 0 * * *'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
issues: write
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: lock-threads
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
action:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: dessant/lock-threads@v5
|
||||||
|
with:
|
||||||
|
issue-inactive-days: '1'
|
||||||
|
process-only: 'issues'
|
||||||
@@ -6,23 +6,23 @@ on:
|
|||||||
- "*"
|
- "*"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
linux:
|
build:
|
||||||
name: Build
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
|
uses: actions/checkout@v4
|
||||||
with:
|
|
||||||
submodules: 'recursive'
|
|
||||||
|
|
||||||
- name: Setup Java 17
|
- name: Setup Java
|
||||||
uses: actions/setup-java@v3
|
uses: actions/setup-java@v4
|
||||||
with:
|
with:
|
||||||
distribution: temurin
|
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.11718014"
|
||||||
|
|
||||||
- name: Install Rust Toolchain
|
- 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
|
- name: Rust Cache
|
||||||
uses: Swatinem/rust-cache@v2
|
uses: Swatinem/rust-cache@v2
|
||||||
@@ -30,86 +30,48 @@ jobs:
|
|||||||
workspaces: "./app/src/main/rust"
|
workspaces: "./app/src/main/rust"
|
||||||
|
|
||||||
- name: CMake Cache
|
- name: CMake Cache
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: app/.cxx
|
||||||
app/.cxx
|
key: cmake-${{ hashFiles('gradle/libs.versions.toml', 'app/build.gradle.kts', 'app/src/main/cpp/CMakeLists.txt') }}
|
||||||
app/src/main/cpp/nettle/nettle
|
restore-keys: cmake-
|
||||||
key: ${{ runner.os }}-cmake-${{ hashFiles('gradle/libs.versions.toml', 'app/build.gradle.kts', 'app/src/main/cpp/CMakeLists.txt') }}
|
|
||||||
restore-keys: ${{ runner.os }}-cmake-
|
|
||||||
|
|
||||||
- name: Gradle Build
|
- name: Gradle Build
|
||||||
uses: gradle/gradle-build-action@v2
|
uses: gradle/actions/setup-gradle@v3
|
||||||
with:
|
with:
|
||||||
arguments: assembleRelease
|
arguments: assembleRelease
|
||||||
|
|
||||||
- name: Rename Apks
|
- name: Rename Apks
|
||||||
run: |
|
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/default/release/app-default-universal-release.apk EhViewer-${{ github.ref_name }}-default-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/default/release/app-default-arm64-v8a-release.apk EhViewer-${{ github.ref_name }}-default-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/default/release/app-default-x86_64-release.apk EhViewer-${{ github.ref_name }}-default-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/default/release/app-default-armeabi-v7a-release.apk EhViewer-${{ github.ref_name }}-default-armeabi-v7a.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/defaultRelease/mapping.txt EhViewer-${{ github.ref_name }}-default-mapping.txt
|
||||||
mv app/build/outputs/mapping/defaultOssRelease/mapping.txt EhViewer-${{ github.ref_name }}-default-oss-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/native-debug-symbols/defaultOssRelease/native-debug-symbols.zip EhViewer-${{ github.ref_name }}-default-oss-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/marshmallow/release/app-marshmallow-universal-release.apk EhViewer-${{ github.ref_name }}-marshmallow-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/marshmallow/release/app-marshmallow-arm64-v8a-release.apk EhViewer-${{ github.ref_name }}-marshmallow-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/marshmallow/release/app-marshmallow-x86_64-release.apk EhViewer-${{ github.ref_name }}-marshmallow-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/marshmallow/release/app-marshmallow-armeabi-v7a-release.apk EhViewer-${{ github.ref_name }}-marshmallow-armeabi-v7a.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/marshmallowRelease/mapping.txt EhViewer-${{ github.ref_name }}-marshmallow-mapping.txt
|
||||||
mv app/build/outputs/mapping/defaultGmsRelease/mapping.txt EhViewer-${{ github.ref_name }}-default-gms-mapping.txt
|
mv app/build/outputs/native-debug-symbols/marshmallowRelease/native-debug-symbols.zip EhViewer-${{ github.ref_name }}-marshmallow-native-debug-symbols.zip
|
||||||
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
|
|
||||||
|
|
||||||
- name: Releases
|
- name: Releases
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
body: Bump Version
|
body: Bump Version
|
||||||
files: |
|
files: |
|
||||||
EhViewer-${{ github.ref_name }}-default-oss-universal.apk
|
EhViewer-${{ github.ref_name }}-default-universal.apk
|
||||||
EhViewer-${{ github.ref_name }}-default-oss-arm64-v8a.apk
|
EhViewer-${{ github.ref_name }}-default-arm64-v8a.apk
|
||||||
EhViewer-${{ github.ref_name }}-default-oss-x86_64.apk
|
EhViewer-${{ github.ref_name }}-default-x86_64.apk
|
||||||
EhViewer-${{ github.ref_name }}-default-oss-x86.apk
|
EhViewer-${{ github.ref_name }}-default-armeabi-v7a.apk
|
||||||
EhViewer-${{ github.ref_name }}-default-oss-armeabi-v7a.apk
|
EhViewer-${{ github.ref_name }}-default-mapping.txt
|
||||||
EhViewer-${{ github.ref_name }}-default-oss-mapping.txt
|
EhViewer-${{ github.ref_name }}-default-native-debug-symbols.zip
|
||||||
EhViewer-${{ github.ref_name }}-default-oss-native-debug-symbols.zip
|
|
||||||
|
|
||||||
EhViewer-${{ github.ref_name }}-default-gms-universal.apk
|
EhViewer-${{ github.ref_name }}-marshmallow-universal.apk
|
||||||
EhViewer-${{ github.ref_name }}-default-gms-arm64-v8a.apk
|
EhViewer-${{ github.ref_name }}-marshmallow-arm64-v8a.apk
|
||||||
EhViewer-${{ github.ref_name }}-default-gms-x86_64.apk
|
EhViewer-${{ github.ref_name }}-marshmallow-x86_64.apk
|
||||||
EhViewer-${{ github.ref_name }}-default-gms-x86.apk
|
EhViewer-${{ github.ref_name }}-marshmallow-armeabi-v7a.apk
|
||||||
EhViewer-${{ github.ref_name }}-default-gms-armeabi-v7a.apk
|
EhViewer-${{ github.ref_name }}-marshmallow-mapping.txt
|
||||||
EhViewer-${{ github.ref_name }}-default-gms-mapping.txt
|
EhViewer-${{ github.ref_name }}-marshmallow-native-debug-symbols.zip
|
||||||
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
|
|
||||||
@@ -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
|
/local.properties
|
||||||
.DS_Store
|
.DS_Store
|
||||||
/build
|
/build
|
||||||
/src
|
/.kotlin
|
||||||
*.iml
|
*.iml
|
||||||
.idea
|
.idea
|
||||||
/captures
|
/captures
|
||||||
google-services.json
|
|
||||||
@@ -0,0 +1,287 @@
|
|||||||
|
# 变更日志
|
||||||
|
|
||||||
|
## [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]
|
||||||
|
|
||||||
|
### 重大变更
|
||||||
|
|
||||||
|
* 后备 HTTP 引擎变更为 Apache HttpClient 5
|
||||||
|
* 类别和高级搜索选项移至搜索栏下方
|
||||||
|
|
||||||
|
### 新功能
|
||||||
|
|
||||||
|
* 支持在搜索时指定画廊语言
|
||||||
|
|
||||||
|
### 改进
|
||||||
|
|
||||||
|
* 在下载中阅读画廊时会更新历史记录
|
||||||
|
* 多处性能改进
|
||||||
|
|
||||||
|
### Bug 修复
|
||||||
|
|
||||||
|
* 修复无法由外部应用进入画廊页面 [#436](https://github.com/FooIbar/EhViewer/issues/436)
|
||||||
|
* 修复点击精确到页的画廊链接闪退 [#430](https://github.com/FooIbar/EhViewer/issues/430)
|
||||||
|
* 修复无法搜索本地收藏 [#491](https://github.com/FooIbar/EhViewer/issues/491)
|
||||||
|
* 修复 CI 版本不显示变更日志
|
||||||
|
|
||||||
|
|
||||||
|
## [1.9.0] - 2023-12-01
|
||||||
|
|
||||||
|
### 重大变更
|
||||||
|
|
||||||
|
* 除阅读器外全部迁移至 Compose
|
||||||
|
|
||||||
|
### 新功能
|
||||||
|
|
||||||
|
* 设置-高级 中增加实验性滑动手势敏感度设置
|
||||||
|
* 支持通过滑动手势打开右侧抽屉
|
||||||
|
* 支持预见式返回动画,相关设置位于 设置-高级
|
||||||
|
|
||||||
|
### 改进
|
||||||
|
|
||||||
|
* [阅读器] 画廊页数过多时隐藏进度条刻度并禁用触感反馈
|
||||||
|
* 优化原图下载重试策略
|
||||||
|
* 优化小屏设备上的搜索类别布局
|
||||||
|
* 优化过渡动画
|
||||||
|
* 优化历史记录删除动画
|
||||||
|
* 设置-EH 中的缩略图大小设置改为缩略图列数
|
||||||
|
|
||||||
|
### Bug 修复
|
||||||
|
|
||||||
|
* 修复点击通知栏的下载通知闪退 [#175](https://github.com/FooIbar/EhViewer/issues/175)
|
||||||
|
* 修复收藏状态显示错误 [#189](https://github.com/FooIbar/EhViewer/issues/189)
|
||||||
|
* 修复双击导航按钮后无法打开导航抽屉
|
||||||
|
* 修复某些情况下闪退 [#142](https://github.com/FooIbar/EhViewer/issues/142) [#208](https://github.com/FooIbar/EhViewer/issues/208)
|
||||||
|
* 修复收藏界面点击重试崩溃 [#193](https://github.com/FooIbar/EhViewer/issues/193)
|
||||||
|
* 修复本地收藏滚动位置丢失 [#192](https://github.com/FooIbar/EhViewer/issues/192)
|
||||||
|
* 修复手速过快页面切换过快使加载圈圈不自动消失 [#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)
|
||||||
|
* 修复已舍弃画廊上传者解析
|
||||||
|
|
||||||
|
### 已知问题
|
||||||
|
|
||||||
|
* 画廊信息对话框无法通过返回关闭 [#413](https://github.com/FooIbar/EhViewer/issues/413)
|
||||||
@@ -1,15 +1,11 @@
|
|||||||
# Maintenance Mode
|
|
||||||
|
|
||||||
The project is in maintenance mode, will only receive bugfixes.
|
|
||||||
|
|
||||||
<p align="right">
|
<p align="right">
|
||||||
<strong>English</strong>
|
<strong>English</strong>
|
||||||
<span> | </span>
|
<span> | </span>
|
||||||
<a href="https://github.com/FooIbar/EhViewer/blob/1.8.x.x/docs/README/zh-cn.md">
|
<a href="/docs/README/zh-cn.md">
|
||||||
简体中文
|
简体中文
|
||||||
</a>
|
</a>
|
||||||
<span> | </span>
|
<span> | </span>
|
||||||
<a href="https://github.com/FooIbar/EhViewer/blob/1.8.x.x/docs/README/zh-tw.md">
|
<a href="/docs/README/zh-tw.md">
|
||||||
正體中文
|
正體中文
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
@@ -23,15 +19,12 @@ The project is in maintenance mode, will only receive bugfixes.
|
|||||||
<a href="https://github.com/FooIbar/EhViewer/actions/workflows/ci.yml">
|
<a href="https://github.com/FooIbar/EhViewer/actions/workflows/ci.yml">
|
||||||
<img src="https://github.com/FooIbar/EhViewer/actions/workflows/ci.yml/badge.svg" alt="Github Actions">
|
<img src="https://github.com/FooIbar/EhViewer/actions/workflows/ci.yml/badge.svg" alt="Github Actions">
|
||||||
</a>
|
</a>
|
||||||
<a href="https://github.com/FooIbar/EhViewer/blob/1.8.x.x/LICENSE">
|
<a href="/LICENSE">
|
||||||
<img src="https://img.shields.io/github/license/FooIbar/EhViewer" alt="LICENSE">
|
<img src="https://img.shields.io/github/license/FooIbar/EhViewer" alt="LICENSE">
|
||||||
</a>
|
</a>
|
||||||
<a href="https://www.codefactor.io/repository/github/FooIbar/EhViewer">
|
<a href="https://www.codefactor.io/repository/github/FooIbar/EhViewer">
|
||||||
<img src="https://www.codefactor.io/repository/github/FooIbar/EhViewer/badge" alt="CodeFactor">
|
<img src="https://www.codefactor.io/repository/github/FooIbar/EhViewer/badge" alt="CodeFactor">
|
||||||
</a>
|
</a>
|
||||||
<a href="https://github.com/FooIbar/EhViewer/releases">
|
|
||||||
<img src="https://img.shields.io/github/downloads/FooIbar/EhViewer/total.svg" alt="Downloads">
|
|
||||||
</a>
|
|
||||||
<a href="https://github.com/FooIbar/EhViewer/releases">
|
<a href="https://github.com/FooIbar/EhViewer/releases">
|
||||||
<img src="https://img.shields.io/github/v/release/FooIbar/EhViewer" alt="Release">
|
<img src="https://img.shields.io/github/v/release/FooIbar/EhViewer" alt="Release">
|
||||||
</a>
|
</a>
|
||||||
@@ -42,23 +35,23 @@ The project is in maintenance mode, will only receive bugfixes.
|
|||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<h3>
|
<h3>
|
||||||
<a href="https://github.com/FooIbar/EhViewer#description">
|
<a href="#description">
|
||||||
Description
|
Description
|
||||||
</a>
|
</a>
|
||||||
<span> | </span>
|
<span> | </span>
|
||||||
<a href="https://github.com/FooIbar/EhViewer#download">
|
<a href="#download">
|
||||||
Download
|
Download
|
||||||
</a>
|
</a>
|
||||||
<span> | </span>
|
<span> | </span>
|
||||||
<a href="https://github.com/FooIbar/EhViewer#screenshot">
|
<a href="#screenshot">
|
||||||
Screenshot
|
Screenshot
|
||||||
</a>
|
</a>
|
||||||
<span> | </span>
|
<span> | </span>
|
||||||
<a href="https://github.com/FooIbar/EhViewer#thanks">
|
<a href="#thanks">
|
||||||
Thanks
|
Thanks
|
||||||
</a>
|
</a>
|
||||||
<span> | </span>
|
<span> | </span>
|
||||||
<a href="https://github.com/FooIbar/EhViewer#license">
|
<a href="#license">
|
||||||
License
|
License
|
||||||
</a>
|
</a>
|
||||||
</h3>
|
</h3>
|
||||||
@@ -73,16 +66,10 @@ and [Dynamic Color](https://m3.material.io/styles/color/dynamic-color/overview)
|
|||||||
|
|
||||||
# Download
|
# Download
|
||||||
|
|
||||||
| Flavor | Feature |
|
| Flavor | Feature |
|
||||||
|-|-|
|
|-------------|----------------------------------|
|
||||||
| Default | Android 9+, full support |
|
| Default | Android 8.0+, full support |
|
||||||
| Marshmallow | Android 6.0-8.1, limited support* |
|
| Marshmallow | Android 6.0-7.1, limited support |
|
||||||
| OSS | FOSS dependencies** |
|
|
||||||
| GMS | Proprietary GMS component |
|
|
||||||
|
|
||||||
\* Archives and GIFs are not supported
|
|
||||||
|
|
||||||
** Fall back to OkHttp on devices that don't support [HttpEngine](https://developer.android.com/reference/android/net/http/HttpEngine)
|
|
||||||
|
|
||||||
<a href="https://github.com/FooIbar/EhViewer/releases">
|
<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"/>
|
<img alt="Get it on GitHub" src="https://github.com/Ehviewer-Overhauled/Art/blob/master/get-it-on-github.svg" width="200px"/>
|
||||||
@@ -98,16 +85,14 @@ and [Dynamic Color](https://m3.material.io/styles/color/dynamic-color/overview)
|
|||||||
Here is the libraries
|
Here is the libraries
|
||||||
|
|
||||||
- [Arrow](https://arrow-kt.io/)
|
- [Arrow](https://arrow-kt.io/)
|
||||||
- [AOSP & AndroidX](http://source.android.com/)
|
- [AOSP & AndroidX](https://source.android.com/)
|
||||||
- [Kotlin & KotlinX](https://kotlinlang.org/)
|
- [Kotlin & KotlinX](https://kotlinlang.org/)
|
||||||
- [FullDraggableDrawer](https://github.com/PureWriter/FullDraggableDrawer)
|
- [Material Icons](https://github.com/google/material-design-icons)
|
||||||
- [MCA](https://github.com/material-components/material-components-android)
|
- [Ktor](https://ktor.io/)
|
||||||
- [material-design-icons](https://github.com/google/material-design-icons)
|
|
||||||
- [Okhttp](https://square.github.io/okhttp/)
|
|
||||||
- [RikkaX](https://github.com/RikkaApps/RikkaX)
|
|
||||||
- [Libarchive](http://www.libarchive.org/)
|
|
||||||
- [Coil](https://coil-kt.github.io/coil/)
|
- [Coil](https://coil-kt.github.io/coil/)
|
||||||
- [Jsoup](https://jsoup.org/)
|
- [Compose Destinations](https://composedestinations.rafaelcosta.xyz/)
|
||||||
|
- [libarchive](https://www.libarchive.org/)
|
||||||
|
- [jsoup](https://jsoup.org/)
|
||||||
|
|
||||||
Specially thanks [Tachiyomi](https://tachiyomi.org/) for its reader implementation!
|
Specially thanks [Tachiyomi](https://tachiyomi.org/) for its reader implementation!
|
||||||
|
|
||||||
@@ -116,7 +101,7 @@ Specially thanks [Tachiyomi](https://tachiyomi.org/) for its reader implementati
|
|||||||
Copyright 2014-2019 Hippo Seven
|
Copyright 2014-2019 Hippo Seven
|
||||||
Copyright 2020-2022 NekoInverter
|
Copyright 2020-2022 NekoInverter
|
||||||
Copyright 2022-2023 Tarsin Norbin
|
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.
|
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
|
/build
|
||||||
/src/main/java-gen
|
|
||||||
manifest-merger-release-report.txt
|
|
||||||
/src/main/libs
|
|
||||||
/src/main/obj
|
|
||||||
# Intellij
|
|
||||||
*.iml
|
|
||||||
/.cxx
|
/.cxx
|
||||||
+82
-99
@@ -1,8 +1,6 @@
|
|||||||
import com.mikepenz.aboutlibraries.plugin.DuplicateMode.MERGE
|
import com.mikepenz.aboutlibraries.plugin.DuplicateMode.MERGE
|
||||||
import com.mikepenz.aboutlibraries.plugin.DuplicateRule.GROUP
|
import com.mikepenz.aboutlibraries.plugin.DuplicateRule.GROUP
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.time.ZoneOffset
|
|
||||||
import java.time.format.DateTimeFormatter
|
|
||||||
|
|
||||||
val isRelease: Boolean
|
val isRelease: Boolean
|
||||||
get() = gradle.startParameter.taskNames.any { it.contains("Release") }
|
get() = gradle.startParameter.taskNames.any { it.contains("Release") }
|
||||||
@@ -10,26 +8,26 @@ val isRelease: Boolean
|
|||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.android.application)
|
alias(libs.plugins.android.application)
|
||||||
alias(libs.plugins.kotlin.android)
|
alias(libs.plugins.kotlin.android)
|
||||||
|
alias(libs.plugins.kotlin.compose)
|
||||||
alias(libs.plugins.kotlin.parcelize)
|
alias(libs.plugins.kotlin.parcelize)
|
||||||
alias(libs.plugins.kotlin.serialization)
|
alias(libs.plugins.kotlin.serialization)
|
||||||
alias(libs.plugins.ksp)
|
alias(libs.plugins.ksp)
|
||||||
alias(libs.plugins.spotless)
|
alias(libs.plugins.spotless)
|
||||||
alias(libs.plugins.aboutlibrariesPlugin)
|
alias(libs.plugins.aboutlibrariesPlugin)
|
||||||
alias(libs.plugins.rustAndroidPlugin)
|
|
||||||
alias(libs.plugins.composeCompilerReportGenerator)
|
alias(libs.plugins.composeCompilerReportGenerator)
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdk = 34
|
compileSdk = 34
|
||||||
buildToolsVersion = "34.0.0"
|
ndkVersion = "27.0.11718014-beta1"
|
||||||
ndkVersion = "26.1.10909125"
|
androidResources.generateLocaleConfig = true
|
||||||
|
|
||||||
splits {
|
splits {
|
||||||
abi {
|
abi {
|
||||||
isEnable = true
|
isEnable = true
|
||||||
reset()
|
reset()
|
||||||
if (isRelease) {
|
if (isRelease) {
|
||||||
include("arm64-v8a", "x86_64", "armeabi-v7a", "x86")
|
include("arm64-v8a", "x86_64", "armeabi-v7a")
|
||||||
isUniversalApk = true
|
isUniversalApk = true
|
||||||
} else {
|
} else {
|
||||||
include("arm64-v8a", "x86_64")
|
include("arm64-v8a", "x86_64")
|
||||||
@@ -50,22 +48,21 @@ android {
|
|||||||
commandLine = "git rev-parse --short=7 HEAD".split(' ')
|
commandLine = "git rev-parse --short=7 HEAD".split(' ')
|
||||||
}.standardOutput.asText.get().trim()
|
}.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 repoName = providers.exec {
|
val repoName = providers.exec {
|
||||||
commandLine = "git remote get-url origin".split(' ')
|
commandLine = "git remote get-url origin".split(' ')
|
||||||
}.standardOutput.asText.get().trim().removePrefix("https://github.com/").removePrefix("git@github.com:")
|
}.standardOutput.asText.get().trim().removePrefix("https://github.com/").removePrefix("git@github.com:")
|
||||||
.removeSuffix(".git")
|
.removeSuffix(".git")
|
||||||
|
|
||||||
|
val chromeVersion = rootProject.layout.projectDirectory.file("chrome-for-testing/LATEST_RELEASE_STABLE").asFile
|
||||||
|
.readText().substringBefore('.')
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "moe.tarsin.ehviewer"
|
applicationId = "moe.tarsin.ehviewer"
|
||||||
minSdk = 28
|
minSdk = 26
|
||||||
targetSdk = 34
|
targetSdk = 34
|
||||||
versionCode = 180045
|
versionCode = 180056
|
||||||
versionName = "1.8.11.0"
|
versionName = "1.11.5"
|
||||||
|
// versionNameSuffix = "-SNAPSHOT"
|
||||||
resourceConfigurations.addAll(
|
resourceConfigurations.addAll(
|
||||||
listOf(
|
listOf(
|
||||||
"zh",
|
"zh",
|
||||||
@@ -82,43 +79,28 @@ android {
|
|||||||
"nb-rNO",
|
"nb-rNO",
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
buildConfigField("String", "RAW_VERSION_NAME", "\"$versionName${versionNameSuffix.orEmpty()}\"")
|
||||||
buildConfigField("String", "COMMIT_SHA", "\"$commitSha\"")
|
buildConfigField("String", "COMMIT_SHA", "\"$commitSha\"")
|
||||||
buildConfigField("String", "REPO_NAME", "\"$repoName\"")
|
buildConfigField("String", "REPO_NAME", "\"$repoName\"")
|
||||||
|
buildConfigField("String", "CHROME_VERSION", "\"$chromeVersion\"")
|
||||||
ndk {
|
ndk {
|
||||||
debugSymbolLevel = "FULL"
|
debugSymbolLevel = "FULL"
|
||||||
}
|
}
|
||||||
|
externalNativeBuild {
|
||||||
|
cmake {
|
||||||
|
arguments += "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
flavorDimensions += listOf("api", "oss")
|
flavorDimensions += "api"
|
||||||
|
|
||||||
productFlavors {
|
productFlavors {
|
||||||
create("default") {
|
create("default")
|
||||||
dimension = "api"
|
|
||||||
}
|
|
||||||
create("marshmallow") {
|
create("marshmallow") {
|
||||||
dimension = "api"
|
|
||||||
minSdk = 23
|
minSdk = 23
|
||||||
applicationIdSuffix = ".m"
|
applicationIdSuffix = ".m"
|
||||||
versionNameSuffix = "-M"
|
versionNameSuffix = "-M"
|
||||||
externalNativeBuild {
|
|
||||||
cmake {
|
|
||||||
targets += "noop"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
compileOptions {
|
|
||||||
isCoreLibraryDesugaringEnabled = true
|
|
||||||
}
|
|
||||||
lint {
|
|
||||||
checkOnly += setOf("InlinedApi", "NewApi", "UnusedAttribute")
|
|
||||||
error += setOf("InlinedApi", "UnusedAttribute")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
create("oss") {
|
|
||||||
dimension = "oss"
|
|
||||||
}
|
|
||||||
create("gms") {
|
|
||||||
dimension = "oss"
|
|
||||||
versionNameSuffix = "-gms"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,17 +110,21 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
isCoreLibraryDesugaringEnabled = true
|
||||||
|
}
|
||||||
|
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
freeCompilerArgs = listOf(
|
freeCompilerArgs = listOf(
|
||||||
// https://kotlinlang.org/docs/compiler-reference.html#progressive
|
// https://kotlinlang.org/docs/compiler-reference.html#progressive
|
||||||
"-progressive",
|
"-progressive",
|
||||||
"-Xjvm-default=all",
|
"-Xjvm-default=all",
|
||||||
"-Xlambdas=indy",
|
"-Xcontext-receivers",
|
||||||
|
|
||||||
"-opt-in=coil.annotation.ExperimentalCoilApi",
|
"-opt-in=coil3.annotation.ExperimentalCoilApi",
|
||||||
"-opt-in=androidx.compose.foundation.layout.ExperimentalLayoutApi",
|
"-opt-in=androidx.compose.foundation.layout.ExperimentalLayoutApi",
|
||||||
"-opt-in=androidx.compose.material3.ExperimentalMaterial3Api",
|
"-opt-in=androidx.compose.material3.ExperimentalMaterial3Api",
|
||||||
"-opt-in=androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi",
|
"-opt-in=androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi",
|
||||||
"-opt-in=androidx.compose.ui.ExperimentalComposeUiApi",
|
"-opt-in=androidx.compose.ui.ExperimentalComposeUiApi",
|
||||||
"-opt-in=androidx.compose.foundation.ExperimentalFoundationApi",
|
"-opt-in=androidx.compose.foundation.ExperimentalFoundationApi",
|
||||||
"-opt-in=androidx.compose.animation.ExperimentalAnimationApi",
|
"-opt-in=androidx.compose.animation.ExperimentalAnimationApi",
|
||||||
@@ -153,17 +139,13 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
lint {
|
lint {
|
||||||
abortOnError = true
|
disable += setOf("MissingTranslation", "MissingQuantity")
|
||||||
checkReleaseBuilds = false
|
fatal += setOf("NewApi", "InlinedApi")
|
||||||
disable.add("MissingTranslation")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
packaging {
|
packaging {
|
||||||
resources {
|
dex {
|
||||||
excludes += "/META-INF/**"
|
useLegacyPackaging = false
|
||||||
excludes += "/kotlin/**"
|
|
||||||
excludes += "**.txt"
|
|
||||||
excludes += "**.bin"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,11 +157,14 @@ android {
|
|||||||
isShrinkResources = true
|
isShrinkResources = true
|
||||||
proguardFiles("proguard-rules.pro")
|
proguardFiles("proguard-rules.pro")
|
||||||
signingConfig = signConfig
|
signingConfig = signConfig
|
||||||
buildConfigField("String", "BUILD_TIME", "\"$buildTime\"")
|
buildConfigField("long", "BUILD_TIME", "${Instant.now().epochSecond}")
|
||||||
}
|
}
|
||||||
debug {
|
debug {
|
||||||
applicationIdSuffix = ".debug"
|
applicationIdSuffix = ".debug"
|
||||||
buildConfigField("String", "BUILD_TIME", "\"\"")
|
buildConfigField("long", "BUILD_TIME", "0")
|
||||||
|
lint {
|
||||||
|
abortOnError = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,66 +174,80 @@ android {
|
|||||||
viewBinding = true
|
viewBinding = true
|
||||||
}
|
}
|
||||||
|
|
||||||
composeOptions {
|
|
||||||
kotlinCompilerExtensionVersion = libs.versions.compose.compiler.get()
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace = "com.hippo.ehviewer"
|
namespace = "com.hippo.ehviewer"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
composeCompiler {
|
||||||
|
enableNonSkippingGroupOptimization = true
|
||||||
|
enableStrongSkippingMode = true
|
||||||
|
}
|
||||||
|
|
||||||
|
androidComponents {
|
||||||
|
onVariants(selector().withBuildType("release")) {
|
||||||
|
it.packaging.resources.excludes.addAll(
|
||||||
|
"/META-INF/**",
|
||||||
|
"/kotlin/**",
|
||||||
|
"**.txt",
|
||||||
|
"**.bin",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
// https://developer.android.com/jetpack/androidx/releases/activity
|
// https://developer.android.com/jetpack/androidx/releases/activity
|
||||||
implementation(libs.androidx.activity.compose)
|
implementation(libs.androidx.activity.compose)
|
||||||
implementation(libs.androidx.appcompat)
|
implementation(libs.androidx.appcompat)
|
||||||
implementation(libs.androidx.biometric)
|
implementation(libs.androidx.biometric)
|
||||||
implementation(libs.androidx.browser)
|
implementation(libs.androidx.browser)
|
||||||
implementation(libs.androidx.collection)
|
|
||||||
|
|
||||||
// https://developer.android.com/jetpack/androidx/releases/compose-material3
|
// https://developer.android.com/jetpack/androidx/releases/compose-material3
|
||||||
api(platform(libs.compose.bom))
|
api(platform(libs.compose.bom))
|
||||||
implementation(libs.bundles.compose)
|
implementation(libs.bundles.compose)
|
||||||
|
|
||||||
|
implementation(libs.compose.destinations.core)
|
||||||
|
ksp(libs.compose.destinations.compiler)
|
||||||
|
|
||||||
implementation(libs.androidx.core)
|
implementation(libs.androidx.core)
|
||||||
implementation(libs.androidx.core.splashscreen)
|
implementation(libs.androidx.core.splashscreen)
|
||||||
|
|
||||||
implementation(libs.androidx.constraintlayout.compose)
|
implementation(libs.androidx.constraintlayout.compose)
|
||||||
implementation(libs.androidx.coordinatorlayout)
|
implementation(libs.androidx.datastore)
|
||||||
implementation(libs.androidx.fragment)
|
implementation(libs.androidx.graphics.path)
|
||||||
|
|
||||||
// https://developer.android.com/jetpack/androidx/releases/lifecycle
|
// https://developer.android.com/jetpack/androidx/releases/lifecycle
|
||||||
implementation(libs.androidx.lifecycle.process)
|
implementation(libs.androidx.lifecycle.process)
|
||||||
|
implementation(libs.androidx.lifecycle.compose)
|
||||||
// https://developer.android.com/jetpack/androidx/releases/navigation
|
|
||||||
implementation(libs.bundles.androidx.navigation)
|
|
||||||
|
|
||||||
// https://developer.android.com/jetpack/androidx/releases/paging
|
// https://developer.android.com/jetpack/androidx/releases/paging
|
||||||
implementation(libs.bundles.androidx.paging)
|
implementation(libs.androidx.paging.compose)
|
||||||
|
|
||||||
implementation(libs.bundles.androidx.recyclerview)
|
implementation(libs.androidx.recyclerview)
|
||||||
|
|
||||||
// https://developer.android.com/jetpack/androidx/releases/room
|
// https://developer.android.com/jetpack/androidx/releases/room
|
||||||
ksp(libs.androidx.room.compiler)
|
ksp(libs.androidx.room.compiler)
|
||||||
implementation(libs.androidx.room.paging)
|
implementation(libs.androidx.room.paging)
|
||||||
|
|
||||||
implementation(libs.androidx.swiperefreshlayout)
|
|
||||||
implementation(libs.androidx.work.runtime)
|
implementation(libs.androidx.work.runtime)
|
||||||
implementation(libs.photoview) // Dead Dependency
|
implementation(libs.photoview) // Dead Dependency
|
||||||
implementation(libs.directionalviewpager) // Dead Dependency
|
implementation(libs.directionalviewpager) // Dead Dependency
|
||||||
// https://github.com/google/accompanist/releases
|
implementation(libs.material.motion.core)
|
||||||
implementation(libs.bundles.accompanist)
|
|
||||||
implementation(libs.material)
|
|
||||||
|
|
||||||
implementation(libs.bundles.splitties)
|
implementation(libs.bundles.splitties)
|
||||||
|
|
||||||
// https://square.github.io/okhttp/changelogs/changelog/
|
|
||||||
implementation(platform(libs.okhttp.bom))
|
|
||||||
implementation(libs.bundles.okhttp)
|
|
||||||
|
|
||||||
implementation(libs.okio.jvm)
|
implementation(libs.okio.jvm)
|
||||||
|
|
||||||
|
implementation(libs.logcat)
|
||||||
|
|
||||||
|
implementation(libs.diff)
|
||||||
|
|
||||||
implementation(libs.aboutlibraries.core)
|
implementation(libs.aboutlibraries.core)
|
||||||
|
implementation(libs.aboutlibraries.compose.m3)
|
||||||
|
implementation(libs.accompanist.drawable.painter)
|
||||||
|
|
||||||
implementation(libs.insetter) // Dead Dependency
|
implementation(libs.insetter) // Dead Dependency
|
||||||
|
|
||||||
|
implementation(libs.reorderable)
|
||||||
|
|
||||||
implementation(platform(libs.arrow.stack))
|
implementation(platform(libs.arrow.stack))
|
||||||
implementation(libs.arrow.fx.coroutines)
|
implementation(libs.arrow.fx.coroutines)
|
||||||
|
|
||||||
@@ -256,30 +255,31 @@ dependencies {
|
|||||||
implementation(platform(libs.coil.bom))
|
implementation(platform(libs.coil.bom))
|
||||||
implementation(libs.bundles.coil)
|
implementation(libs.bundles.coil)
|
||||||
|
|
||||||
implementation(libs.bundles.ktor)
|
implementation(libs.telephoto.zoomable)
|
||||||
|
|
||||||
|
implementation(libs.ktor.client.core)
|
||||||
|
|
||||||
implementation(libs.bundles.kotlinx.serialization)
|
implementation(libs.bundles.kotlinx.serialization)
|
||||||
|
|
||||||
implementation(libs.kotlinx.coroutines.android)
|
implementation(libs.kotlinx.coroutines.android)
|
||||||
|
implementation(libs.kotlinx.datetime)
|
||||||
implementation(libs.jsoup)
|
implementation(libs.jsoup)
|
||||||
|
|
||||||
debugImplementation(libs.chucker)
|
|
||||||
releaseImplementation(libs.chucker.nop)
|
|
||||||
|
|
||||||
debugImplementation(libs.leakcanary.android)
|
|
||||||
|
|
||||||
coreLibraryDesugaring(libs.desugar)
|
coreLibraryDesugaring(libs.desugar)
|
||||||
|
|
||||||
"gmsImplementation"(libs.bundles.cronet)
|
implementation(libs.cronet.embedded)
|
||||||
|
|
||||||
|
debugImplementation(libs.compose.ui.tooling)
|
||||||
|
implementation(libs.compose.ui.tooling.preview)
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
jvmToolchain(17)
|
jvmToolchain(21)
|
||||||
}
|
}
|
||||||
|
|
||||||
ksp {
|
ksp {
|
||||||
arg("room.schemaLocation", "$projectDir/schemas")
|
arg("room.schemaLocation", "$projectDir/schemas")
|
||||||
arg("room.generateKotlin", "true")
|
arg("compose-destinations.codeGenPackageName", "com.hippo.ehviewer.ui")
|
||||||
}
|
}
|
||||||
|
|
||||||
aboutLibraries {
|
aboutLibraries {
|
||||||
@@ -287,30 +287,13 @@ aboutLibraries {
|
|||||||
duplicationRule = GROUP
|
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()
|
|
||||||
|
|
||||||
spotless {
|
spotless {
|
||||||
kotlin {
|
kotlin {
|
||||||
// https://github.com/diffplug/spotless/issues/111
|
// https://github.com/diffplug/spotless/issues/111
|
||||||
target("src/**/*.kt")
|
target("src/**/*.kt")
|
||||||
ktlint(ktlintVersion)
|
ktlint()
|
||||||
}
|
}
|
||||||
kotlinGradle {
|
kotlinGradle {
|
||||||
ktlint(ktlintVersion)
|
ktlint()
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.configureEach {
|
|
||||||
if (name.startsWith("merge") && name.endsWith("JniLibFolders")) {
|
|
||||||
dependsOn("cargoBuild")
|
|
||||||
// fix mergeDebugJniLibFolders UP-TO-DATE
|
|
||||||
inputs.dir(layout.buildDirectory.dir("rustJniLibs/android"))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Vendored
+14
-18
@@ -1,30 +1,26 @@
|
|||||||
-keepclassmembers class * implements android.os.Parcelable {
|
|
||||||
public static final ** CREATOR;
|
|
||||||
}
|
|
||||||
|
|
||||||
-keepclasseswithmembernames,includedescriptorclasses class * {
|
-keepclasseswithmembernames,includedescriptorclasses class * {
|
||||||
native <methods>;
|
native <methods>;
|
||||||
}
|
}
|
||||||
|
|
||||||
# https://issuetracker.google.com/issues/190382641
|
-keepclassmembers enum * {
|
||||||
-keepclassmembers class kotlin.SafePublicationLazyImpl {
|
public static **[] values();
|
||||||
java.lang.Object _value;
|
public static ** valueOf(java.lang.String);
|
||||||
}
|
}
|
||||||
|
|
||||||
-keep class androidx.viewpager.widget.ViewPager$LayoutParams { int position; }
|
-keepclassmembers class * implements android.os.Parcelable {
|
||||||
|
public static final ** CREATOR;
|
||||||
-keep class com.hippo.ehviewer.client.parser.Torrent { *; }
|
}
|
||||||
-keep class com.hippo.ehviewer.client.parser.GalleryListResult { *; }
|
|
||||||
-keep class com.hippo.ehviewer.client.parser.Limits { *; }
|
|
||||||
-keep class com.hippo.ehviewer.client.data.BaseGalleryInfo { *; }
|
|
||||||
-keep class com.hippo.ehviewer.ui.scene.SearchBarScene { getBinding(); }
|
|
||||||
|
|
||||||
# Ktor logger
|
# Ktor logger
|
||||||
-dontwarn org.slf4j.impl.StaticLoggerBinder
|
-dontwarn org.slf4j.impl.StaticLoggerBinder
|
||||||
|
|
||||||
-keepattributes LineNumberTable,SourceFile
|
-dontwarn org.conscrypt.Conscrypt
|
||||||
-renamesourcefileattribute SourceFile
|
|
||||||
|
# https://issuetracker.google.com/222232895
|
||||||
|
-dontwarn androidx.window.extensions.**
|
||||||
|
-dontwarn androidx.window.sidecar.Sidecar*
|
||||||
|
|
||||||
|
-keepattributes LineNumberTable
|
||||||
|
|
||||||
-repackageclasses
|
|
||||||
-allowaccessmodification
|
-allowaccessmodification
|
||||||
-overloadaggressively
|
-repackageclasses
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
{
|
|
||||||
"formatVersion": 1,
|
|
||||||
"database": {
|
|
||||||
"version": 1,
|
|
||||||
"identityHash": "c801ee62729ba20a203b150211234d5c",
|
|
||||||
"entities": [
|
|
||||||
{
|
|
||||||
"tableName": "OK_HTTP_3_COOKIE",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`NAME` TEXT, `VALUE` TEXT, `EXPIRES_AT` INTEGER NOT NULL, `DOMAIN` TEXT, `PATH` TEXT, `SECURE` INTEGER NOT NULL, `HTTP_ONLY` INTEGER NOT NULL, `PERSISTENT` INTEGER NOT NULL, `HOST_ONLY` INTEGER NOT NULL, `_id` INTEGER NOT NULL, PRIMARY KEY(`_id`))",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "name",
|
|
||||||
"columnName": "NAME",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "value",
|
|
||||||
"columnName": "VALUE",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "expiresAt",
|
|
||||||
"columnName": "EXPIRES_AT",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "domain",
|
|
||||||
"columnName": "DOMAIN",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "path",
|
|
||||||
"columnName": "PATH",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "secure",
|
|
||||||
"columnName": "SECURE",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "httpOnly",
|
|
||||||
"columnName": "HTTP_ONLY",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "persistent",
|
|
||||||
"columnName": "PERSISTENT",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "hostOnly",
|
|
||||||
"columnName": "HOST_ONLY",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "id",
|
|
||||||
"columnName": "_id",
|
|
||||||
"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, 'c801ee62729ba20a203b150211234d5c')"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
{
|
|
||||||
"formatVersion": 1,
|
|
||||||
"database": {
|
|
||||||
"version": 2,
|
|
||||||
"identityHash": "1b80cd29939b0f43934721f1289ba94d",
|
|
||||||
"entities": [
|
|
||||||
{
|
|
||||||
"tableName": "OK_HTTP_3_COOKIE",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`NAME` TEXT NOT NULL, `VALUE` TEXT NOT NULL, `EXPIRES_AT` INTEGER NOT NULL, `DOMAIN` TEXT NOT NULL, `PATH` TEXT NOT NULL, `SECURE` INTEGER NOT NULL, `HTTP_ONLY` INTEGER NOT NULL, `PERSISTENT` INTEGER NOT NULL, `HOST_ONLY` INTEGER NOT NULL, `_id` INTEGER, PRIMARY KEY(`_id`))",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "name",
|
|
||||||
"columnName": "NAME",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "value",
|
|
||||||
"columnName": "VALUE",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "expiresAt",
|
|
||||||
"columnName": "EXPIRES_AT",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "domain",
|
|
||||||
"columnName": "DOMAIN",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "path",
|
|
||||||
"columnName": "PATH",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "secure",
|
|
||||||
"columnName": "SECURE",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "httpOnly",
|
|
||||||
"columnName": "HTTP_ONLY",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "persistent",
|
|
||||||
"columnName": "PERSISTENT",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "hostOnly",
|
|
||||||
"columnName": "HOST_ONLY",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "id",
|
|
||||||
"columnName": "_id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"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, '1b80cd29939b0f43934721f1289ba94d')"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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,86 +0,0 @@
|
|||||||
package com.hippo.ehviewer.cronet
|
|
||||||
|
|
||||||
import com.hippo.ehviewer.Settings
|
|
||||||
import com.hippo.ehviewer.client.CHROME_ACCEPT
|
|
||||||
import com.hippo.ehviewer.client.CHROME_ACCEPT_LANGUAGE
|
|
||||||
import com.hippo.ehviewer.client.EhCookieStore
|
|
||||||
import java.nio.ByteBuffer
|
|
||||||
import kotlin.contracts.InvocationKind
|
|
||||||
import kotlin.contracts.contract
|
|
||||||
import kotlin.coroutines.Continuation
|
|
||||||
import kotlin.coroutines.resume
|
|
||||||
import kotlin.coroutines.resumeWithException
|
|
||||||
import kotlinx.coroutines.coroutineScope
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
|
||||||
import okio.Path.Companion.toOkioPath
|
|
||||||
import org.chromium.net.CronetEngine
|
|
||||||
import org.chromium.net.CronetException
|
|
||||||
import org.chromium.net.UrlRequest
|
|
||||||
import org.chromium.net.UrlResponseInfo
|
|
||||||
import splitties.init.appCtx
|
|
||||||
|
|
||||||
val cronetHttpClient: CronetEngine = CronetEngine.Builder(appCtx).apply {
|
|
||||||
enableBrotli(true)
|
|
||||||
val cache = (appCtx.cacheDir.toOkioPath() / "http_cache").toFile().apply { mkdirs() }
|
|
||||||
setStoragePath(cache.absolutePath)
|
|
||||||
enableHttpCache(CronetEngine.Builder.HTTP_CACHE_DISK_NO_HTTP, 100 * 1024)
|
|
||||||
setUserAgent(Settings.userAgent)
|
|
||||||
}.build()
|
|
||||||
|
|
||||||
class CronetRequest {
|
|
||||||
lateinit var consumer: (ByteBuffer) -> Unit
|
|
||||||
lateinit var onResponse: CronetRequest.(UrlResponseInfo) -> Unit
|
|
||||||
lateinit var request: UrlRequest
|
|
||||||
lateinit var onError: (Throwable) -> Unit
|
|
||||||
lateinit var readerCont: Continuation<Unit>
|
|
||||||
val callback = object : UrlRequest.Callback() {
|
|
||||||
override fun onRedirectReceived(req: UrlRequest, info: UrlResponseInfo, url: String) {
|
|
||||||
req.followRedirect()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResponseStarted(req: UrlRequest, info: UrlResponseInfo) {
|
|
||||||
onResponse(info)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onReadCompleted(req: UrlRequest, info: UrlResponseInfo, data: ByteBuffer) {
|
|
||||||
data.flip()
|
|
||||||
consumer(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSucceeded(req: UrlRequest, info: UrlResponseInfo) {
|
|
||||||
readerCont.resume(Unit)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFailed(req: UrlRequest, info: UrlResponseInfo?, e: CronetException) {
|
|
||||||
onError(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inline fun cronetRequest(url: String, referer: String? = null, conf: UrlRequest.Builder.() -> Unit = {}) = CronetRequest().apply {
|
|
||||||
request = cronetHttpClient.newUrlRequestBuilder(url, callback, cronetHttpClientExecutor).apply {
|
|
||||||
addHeader("Cookie", EhCookieStore.getCookieHeader(url.toHttpUrl()))
|
|
||||||
addHeader("Accept", CHROME_ACCEPT)
|
|
||||||
addHeader("Accept-Language", CHROME_ACCEPT_LANGUAGE)
|
|
||||||
referer?.let { addHeader("Referer", it) }
|
|
||||||
}.apply(conf).build()
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend inline fun <R> CronetRequest.execute(crossinline callback: suspend CronetRequest.(UrlResponseInfo) -> R): R {
|
|
||||||
contract {
|
|
||||||
callsInPlace(callback, InvocationKind.EXACTLY_ONCE)
|
|
||||||
}
|
|
||||||
return coroutineScope {
|
|
||||||
suspendCancellableCoroutine { cont ->
|
|
||||||
onResponse = { launch { cont.resume(callback(it)) } }
|
|
||||||
cont.invokeOnCancellation { request.cancel() }
|
|
||||||
onError = { cont.resumeWithException(it) }
|
|
||||||
request.start()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun UrlRequest.Builder.noCache(): UrlRequest.Builder = disableCache()
|
|
||||||
fun UrlResponseInfo.getHeadersMap(): Map<String, List<String>> = allHeaders
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
package com.hippo.ehviewer.util
|
|
||||||
|
|
||||||
import com.google.android.gms.net.CronetProviderInstaller
|
|
||||||
import com.hippo.ehviewer.Settings
|
|
||||||
import org.chromium.net.CronetProvider
|
|
||||||
import splitties.init.appCtx
|
|
||||||
|
|
||||||
val isCronetSupported: Boolean
|
|
||||||
get() = Settings.enableCronet && CronetProvider.getAllProviders(appCtx).any { it.isEnabled }.also {
|
|
||||||
if (!it) {
|
|
||||||
CronetProviderInstaller.installProvider(appCtx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -23,21 +23,19 @@
|
|||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:appCategory="image"
|
android:appCategory="image"
|
||||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||||
|
android:enableOnBackInvokedCallback="true"
|
||||||
android:fullBackupContent="@xml/backup_scheme"
|
android:fullBackupContent="@xml/backup_scheme"
|
||||||
android:hasFragileUserData="true"
|
android:hasFragileUserData="true"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:localeConfig="@xml/locale_config"
|
android:largeHeap="@bool/large_heap"
|
||||||
android:networkSecurityConfig="@xml/network_security_config"
|
android:networkSecurityConfig="@xml/network_security_config"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/AppTheme"
|
android:theme="@style/AppTheme"
|
||||||
tools:targetApi="tiramisu">
|
tools:targetApi="tiramisu">
|
||||||
<activity
|
<profileable android:shell="true" />
|
||||||
android:name=".ui.ConfigureActivity"
|
|
||||||
android:exported="false"
|
|
||||||
android:launchMode="singleTask"
|
|
||||||
android:windowSoftInputMode="adjustResize" />
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.MainActivity"
|
android:name=".ui.MainActivity"
|
||||||
android:banner="@mipmap/ic_leanback_banner"
|
android:banner="@mipmap/ic_leanback_banner"
|
||||||
@@ -135,8 +133,6 @@
|
|||||||
android:launchMode="singleTask"
|
android:launchMode="singleTask"
|
||||||
android:taskAffinity=""
|
android:taskAffinity=""
|
||||||
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
|
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
|
||||||
<activity
|
|
||||||
android:name=".ui.WebViewActivity" />
|
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
|
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,29 @@
|
|||||||
|
From 2fa97f75b64c05da506968fa9a1711716f2d45a0 Mon Sep 17 00:00:00 2001
|
||||||
|
From: FooIbar <118464521+FooIbar@users.noreply.github.com>
|
||||||
|
Date: Tue, 26 Sep 2023 00:08:03 +0800
|
||||||
|
Subject: [PATCH 1/2] Hide symbols
|
||||||
|
|
||||||
|
---
|
||||||
|
CMakeLists.txt | 4 ++--
|
||||||
|
1 file changed, 2 insertions(+), 2 deletions(-)
|
||||||
|
|
||||||
|
diff --git a/CMakeLists.txt b/CMakeLists.txt
|
||||||
|
index ec97e4c7..4d859341 100644
|
||||||
|
--- a/CMakeLists.txt
|
||||||
|
+++ b/CMakeLists.txt
|
||||||
|
@@ -2129,10 +2129,10 @@ SET(OLD_CMAKE_REQUIRED_FLAGS "${CMAKE_REQUIRED_FLAGS}")
|
||||||
|
SET(CMAKE_REQUIRED_FLAGS "${CMAKE_REQUIRED_FLAGS} -fvisibility=hidden -Werror")
|
||||||
|
CHECK_C_SOURCE_COMPILES("void __attribute__((visibility(\"default\"))) foo(void);
|
||||||
|
int main() { return 0; }" HAVE_VISIBILITY_ATTR)
|
||||||
|
-IF (HAVE_VISIBILITY_ATTR)
|
||||||
|
+IF (0)
|
||||||
|
SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fvisibility=hidden")
|
||||||
|
ADD_DEFINITIONS(-D__LIBARCHIVE_ENABLE_VISIBILITY)
|
||||||
|
-ENDIF(HAVE_VISIBILITY_ATTR)
|
||||||
|
+ENDIF(0)
|
||||||
|
SET(CMAKE_REQUIRED_FLAGS "${OLD_CMAKE_REQUIRED_FLAGS}")
|
||||||
|
|
||||||
|
# Generate "config.h" from "build/cmake/config.h.in"
|
||||||
|
--
|
||||||
|
2.34.1
|
||||||
|
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
From 2dcc8be937499022c37678eb6bf4bd026b6fcbb0 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 2/2] Insert link libs
|
||||||
|
|
||||||
|
---
|
||||||
|
CMakeLists.txt | 2 +-
|
||||||
|
1 file changed, 1 insertion(+), 1 deletion(-)
|
||||||
|
|
||||||
|
diff --git a/CMakeLists.txt b/CMakeLists.txt
|
||||||
|
index 4d859341..1cd1cd47 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,5 @@
|
|||||||
cmake_minimum_required(VERSION 3.14)
|
cmake_minimum_required(VERSION 3.14)
|
||||||
project(ehviewer C ASM)
|
project(ehviewer C)
|
||||||
include(FetchContent)
|
include(FetchContent)
|
||||||
|
|
||||||
if (NOT CMAKE_BUILD_TYPE STREQUAL "Debug")
|
if (NOT CMAKE_BUILD_TYPE STREQUAL "Debug")
|
||||||
@@ -16,32 +16,32 @@ if (NOT CMAKE_BUILD_TYPE STREQUAL "Debug")
|
|||||||
-mllvm -polly-invariant-load-hoisting")
|
-mllvm -polly-invariant-load-hoisting")
|
||||||
endif (NOT CMAKE_BUILD_TYPE STREQUAL "Debug")
|
endif (NOT CMAKE_BUILD_TYPE STREQUAL "Debug")
|
||||||
|
|
||||||
# Change the default path of fetch content to avoid downloading the same dependencies per architecture
|
option(BUILD_TESTING OFF)
|
||||||
get_filename_component(deps "../_deps" REALPATH BASE_DIR "${CMAKE_BINARY_DIR}")
|
|
||||||
set(FETCHCONTENT_BASE_DIR ${deps})
|
|
||||||
|
|
||||||
set(BUILD_TESTING 0)
|
FetchContent_Declare(
|
||||||
|
liblzma
|
||||||
FetchContent_Declare(liblzma
|
GIT_REPOSITORY https://github.com/tukaani-project/xz.git
|
||||||
GIT_REPOSITORY https://github.com/FooIbar/xz.git
|
GIT_TAG v5.6.1
|
||||||
GIT_TAG v5.4.4
|
PATCH_COMMAND git apply ${CMAKE_CURRENT_LIST_DIR}/0001-Don-t-build-other-binaries.patch || cd .
|
||||||
)
|
)
|
||||||
|
|
||||||
FetchContent_MakeAvailable(liblzma)
|
FetchContent_MakeAvailable(liblzma)
|
||||||
include_directories(${liblzma_SOURCE_DIR}/src/liblzma/api)
|
include_directories(${liblzma_SOURCE_DIR}/src/liblzma/api)
|
||||||
|
|
||||||
# Build GNUTLS libnettle
|
# Build GNUTLS libnettle
|
||||||
FetchContent_Declare(nettle
|
FetchContent_Declare(
|
||||||
|
nettle
|
||||||
URL https://ftp.gnu.org/gnu/nettle/nettle-3.9.1.tar.gz
|
URL https://ftp.gnu.org/gnu/nettle/nettle-3.9.1.tar.gz
|
||||||
URL_MD5 29fcd2dec6bf5b48e5e3ffb3cbc4779e
|
URL_MD5 29fcd2dec6bf5b48e5e3ffb3cbc4779e
|
||||||
SOURCE_DIR ${CMAKE_CURRENT_LIST_DIR}/nettle/nettle
|
SOURCE_DIR nettle-src/nettle
|
||||||
)
|
)
|
||||||
|
|
||||||
FetchContent_MakeAvailable(nettle)
|
FetchContent_MakeAvailable(nettle)
|
||||||
|
set(NETTLE_DIR ${nettle_SOURCE_DIR})
|
||||||
add_subdirectory(nettle)
|
add_subdirectory(nettle)
|
||||||
|
|
||||||
# Configure libnettle support for libarchive
|
# Configure libnettle support for libarchive
|
||||||
include_directories(nettle)
|
include_directories(${CMAKE_CURRENT_BINARY_DIR}/nettle-src)
|
||||||
include_directories(.)
|
include_directories(.)
|
||||||
set(HAVE_LIBNETTLE 1)
|
set(HAVE_LIBNETTLE 1)
|
||||||
set(HAVE_NETTLE_AES_H 1)
|
set(HAVE_NETTLE_AES_H 1)
|
||||||
@@ -67,16 +67,26 @@ option(ENABLE_TEST OFF)
|
|||||||
# Configure libarchive link's static lib
|
# Configure libarchive link's static lib
|
||||||
SET(LIBARCHIVE_CUSTOM_LIBS "nettle" "liblzma")
|
SET(LIBARCHIVE_CUSTOM_LIBS "nettle" "liblzma")
|
||||||
|
|
||||||
FetchContent_Declare(libarchive
|
FetchContent_Declare(
|
||||||
GIT_REPOSITORY https://github.com/FooIbar/libarchive.git
|
libarchive
|
||||||
GIT_TAG v3.7.2
|
GIT_REPOSITORY https://github.com/libarchive/libarchive.git
|
||||||
)
|
GIT_TAG v3.7.4
|
||||||
|
PATCH_COMMAND git apply ${CMAKE_CURRENT_LIST_DIR}/0001-Hide-symbols.patch ${CMAKE_CURRENT_LIST_DIR}/0002-Insert-link-libs.patch || cd .
|
||||||
|
)
|
||||||
|
|
||||||
FetchContent_MakeAvailable(libarchive)
|
FetchContent_MakeAvailable(libarchive)
|
||||||
include_directories(${libarchive_SOURCE_DIR}/libarchive)
|
include_directories(${libarchive_SOURCE_DIR}/libarchive)
|
||||||
|
|
||||||
# Build and link our app's native lib
|
FetchContent_Declare(
|
||||||
add_library(${PROJECT_NAME} SHARED archive.c gifutils.c natsort/strnatcmp.c)
|
Corrosion
|
||||||
target_link_libraries(${PROJECT_NAME} archive_static log)
|
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 natsort/strnatcmp.c)
|
||||||
|
target_link_libraries(${PROJECT_NAME} ehviewer_rust archive_static log jnigraphics)
|
||||||
+128
-92
@@ -34,7 +34,6 @@
|
|||||||
|
|
||||||
#include "natsort/strnatcmp.h"
|
#include "natsort/strnatcmp.h"
|
||||||
#include "ehviewer.h"
|
#include "ehviewer.h"
|
||||||
#include "spinlock.h"
|
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
int using;
|
int using;
|
||||||
@@ -49,18 +48,18 @@ typedef struct {
|
|||||||
size_t size;
|
size_t size;
|
||||||
} entry;
|
} entry;
|
||||||
|
|
||||||
_Atomic mcs_lock ctx_lock;
|
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||||
static archive_ctx **ctx_pool;
|
static archive_ctx **ctx_pool;
|
||||||
#define CTX_POOL_SIZE 20
|
#define CTX_POOL_SIZE 20
|
||||||
|
|
||||||
static void *mempool = NULL;
|
static void *mempool = NULL;
|
||||||
static size_t *mempoolofs = 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_ADDR_BY_SORTED_IDX(x) (mempool + (index ? mempoolofs[index - 1] : 0))
|
||||||
#define MEMPOOL_SIZE (mempoolofs[entryCount - 1])
|
#define MEMPOOL_SIZE (mempoolofs[entryCount - 1])
|
||||||
#define MEMPOOL_ENTITY_SIZE(x) PAGE_ALIGN(entries[x].size)
|
|
||||||
|
|
||||||
#define PROT_RW (PROT_WRITE | PROT_READ)
|
#define PROT_RW (PROT_WRITE | PROT_READ)
|
||||||
#define MAP_ANON_POOL (MAP_ANONYMOUS | MAP_NORESERVE | MAP_PRIVATE | MAP_UNINITIALIZED)
|
#define MAP_ANON_POOL (MAP_ANONYMOUS | MAP_NORESERVE | MAP_PRIVATE | MAP_UNINITIALIZED)
|
||||||
@@ -86,6 +85,8 @@ const char supportExt[10][6] = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
static inline int filename_is_playable_file(const char *name) {
|
static inline int filename_is_playable_file(const char *name) {
|
||||||
|
if (!name)
|
||||||
|
return false;
|
||||||
const char *dotptr = strrchr(name, '.');
|
const char *dotptr = strrchr(name, '.');
|
||||||
if (!dotptr++)
|
if (!dotptr++)
|
||||||
return false;
|
return false;
|
||||||
@@ -96,26 +97,39 @@ static inline int filename_is_playable_file(const char *name) {
|
|||||||
return false;
|
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) {
|
static inline int compare_entries(const void *a, const void *b) {
|
||||||
const char *fa = ((entry *) a)->filename;
|
const char *fa = ((entry *) a)->filename;
|
||||||
const char *fb = ((entry *) b)->filename;
|
const char *fb = ((entry *) b)->filename;
|
||||||
return strnatcmp(fa, fb);
|
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;
|
long count = 0;
|
||||||
while (archive_read_next_header(ctx->arc, &ctx->entry) == ARCHIVE_OK) {
|
while (archive_read_next_header(ctx->arc, &ctx->entry) == ARCHIVE_OK) {
|
||||||
const char *name = archive_entry_pathname(ctx->entry);
|
const char *name = archive_entry_pathname2(ctx->entry);
|
||||||
if (filename_is_playable_file(name)) {
|
if (archive_entry_is_file(ctx->entry) && filename_is_playable_file(name)) {
|
||||||
entries[count].filename = strdup(name);
|
entries[count].filename = strdup(name);
|
||||||
entries[count].index = count;
|
entries[count].index = count;
|
||||||
entries[count].size = archive_entry_size(ctx->entry);
|
entries[count].size = archive_entry_size(ctx->entry);
|
||||||
count++;
|
count++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!count)
|
if (sort)
|
||||||
LOGE("%s", archive_error_string(ctx->arc));
|
qsort(entries, entryCount, sizeof(entry), compare_entries);
|
||||||
qsort(entries, entryCount, sizeof(entry), compare_entries);
|
|
||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,39 +149,27 @@ static bool archive_prealloc_mempool() {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#define ADDR_IN_FILE_MAPPING(addr) (addr >= archiveAddr && addr < archiveAddr + archiveSize)
|
||||||
// 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)
|
|
||||||
|
|
||||||
static void mempool_release_pages(void *addr, size_t size) {
|
static void mempool_release_pages(void *addr, size_t size) {
|
||||||
size = PAGE_ALIGN(size);
|
size = PAGE_ALIGN(size);
|
||||||
munlock(addr, size);
|
madvise_log_if_error(addr, size, MADV_DONTNEED);
|
||||||
int ret = madvise(addr, size, MADV_FREE);
|
}
|
||||||
if (ret == -EINVAL) madvise(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) {
|
static long archive_list_all_entries(archive_ctx *ctx) {
|
||||||
long count = 0;
|
long count = 0;
|
||||||
while (archive_read_next_header(ctx->arc, &ctx->entry) == ARCHIVE_OK)
|
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++;
|
count++;
|
||||||
if (!count)
|
|
||||||
LOGE("%s", archive_error_string(ctx->arc));
|
|
||||||
return count;
|
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) {
|
static int archive_skip_to_index(archive_ctx *ctx, int index) {
|
||||||
while (archive_read_next_header(ctx->arc, &ctx->entry) == ARCHIVE_OK) {
|
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;
|
continue;
|
||||||
if (ctx->next_index++ == index) {
|
if (ctx->next_index++ == index) {
|
||||||
return ctx->next_index - 1;
|
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) {
|
static int archive_get_ctx(archive_ctx **ctxptr, int idx) {
|
||||||
int ret;
|
int ret;
|
||||||
archive_ctx *ctx = NULL;
|
archive_ctx *ctx = NULL;
|
||||||
mcs_lock_t local_lock;
|
pthread_mutex_lock(&mutex);
|
||||||
lock_mcs(&ctx_lock, &local_lock);
|
|
||||||
for (int i = 0; i < CTX_POOL_SIZE; i++) {
|
for (int i = 0; i < CTX_POOL_SIZE; i++) {
|
||||||
if (!ctx_pool[i])
|
if (!ctx_pool[i])
|
||||||
continue;
|
continue;
|
||||||
@@ -240,7 +241,7 @@ static int archive_get_ctx(archive_ctx **ctxptr, int idx) {
|
|||||||
}
|
}
|
||||||
if (ctx)
|
if (ctx)
|
||||||
ctx->using = 1;
|
ctx->using = 1;
|
||||||
unlock_mcs(&ctx_lock, &local_lock);
|
pthread_mutex_unlock(&mutex);
|
||||||
|
|
||||||
if (!ctx) {
|
if (!ctx) {
|
||||||
archive_ctx *victimCtx = NULL;
|
archive_ctx *victimCtx = NULL;
|
||||||
@@ -249,7 +250,7 @@ static int archive_get_ctx(archive_ctx **ctxptr, int idx) {
|
|||||||
ret = archive_alloc_ctx(&ctx);
|
ret = archive_alloc_ctx(&ctx);
|
||||||
if (ret)
|
if (ret)
|
||||||
return ret;
|
return ret;
|
||||||
lock_mcs(&ctx_lock, &local_lock);
|
pthread_mutex_lock(&mutex);
|
||||||
for (int i = 0; i < CTX_POOL_SIZE; i++) {
|
for (int i = 0; i < CTX_POOL_SIZE; i++) {
|
||||||
if (!ctx_pool[i]) {
|
if (!ctx_pool[i]) {
|
||||||
ctx_pool[i] = ctx;
|
ctx_pool[i] = ctx;
|
||||||
@@ -264,7 +265,7 @@ static int archive_get_ctx(archive_ctx **ctxptr, int idx) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (replace) ctx_pool[victimIdx] = ctx;
|
if (replace) ctx_pool[victimIdx] = ctx;
|
||||||
unlock_mcs(&ctx_lock, &local_lock);
|
pthread_mutex_unlock(&mutex);
|
||||||
if (replace) archive_release_ctx(victimCtx);
|
if (replace) archive_release_ctx(victimCtx);
|
||||||
}
|
}
|
||||||
ret = archive_skip_to_index(ctx, idx);
|
ret = archive_skip_to_index(ctx, idx);
|
||||||
@@ -279,19 +280,17 @@ static int archive_get_ctx(archive_ctx **ctxptr, int idx) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
JNIEXPORT jint JNICALL
|
JNIEXPORT jint JNICALL
|
||||||
Java_com_hippo_ehviewer_jni_ArchiveKt_openArchive(JNIEnv *env, jclass thiz, jint fd,
|
Java_com_hippo_ehviewer_jni_ArchiveKt_openArchive(JNIEnv *env, jclass thiz, jint fd, jlong size, jboolean sort_entries) {
|
||||||
jlong size) {
|
|
||||||
EH_UNUSED(env);
|
EH_UNUSED(env);
|
||||||
EH_UNUSED(thiz);
|
EH_UNUSED(thiz);
|
||||||
|
page_size = getpagesize();
|
||||||
archive_ctx *ctx = NULL;
|
archive_ctx *ctx = NULL;
|
||||||
int mmap_flags = MAP_PRIVATE;
|
archiveAddr = mmap(0, size, PROT_READ, MAP_PRIVATE, fd, 0);
|
||||||
archiveAddr = mmap64(0, size, PROT_READ, mmap_flags, fd, 0);
|
|
||||||
if (archiveAddr == MAP_FAILED) {
|
if (archiveAddr == MAP_FAILED) {
|
||||||
LOGE("%s%s", "mmap64 failed with error ", strerror(errno));
|
LOGE("%s%s", "mmap failed with error ", strerror(errno));
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
archiveSize = size;
|
archiveSize = size;
|
||||||
madvise_log_if_error(archiveAddr, archiveSize, MADV_WILLNEED);
|
|
||||||
ctx_pool = calloc(CTX_POOL_SIZE, sizeof(archive_ctx **));
|
ctx_pool = calloc(CTX_POOL_SIZE, sizeof(archive_ctx **));
|
||||||
if (!ctx_pool) {
|
if (!ctx_pool) {
|
||||||
LOGE("Allocate archive ctx pool failed:ENOMEM");
|
LOGE("Allocate archive ctx pool failed:ENOMEM");
|
||||||
@@ -305,6 +304,11 @@ Java_com_hippo_ehviewer_jni_ArchiveKt_openArchive(JNIEnv *env, jclass thiz, jint
|
|||||||
} else {
|
} else {
|
||||||
entryCount = archive_list_all_entries(ctx);
|
entryCount = archive_list_all_entries(ctx);
|
||||||
LOGI("%s%zu%s", "Found ", entryCount, " image entries in archive");
|
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
|
// We must read through the file|vm then we can know whether it is encrypted
|
||||||
int encryptRet = archive_read_has_encrypted_entries(ctx->arc);
|
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));
|
LOGE("%s%s", "Archive open failed:", archive_error_string(ctx->arc));
|
||||||
} else {
|
} else {
|
||||||
entries = calloc(entryCount, sizeof(entry));
|
entries = calloc(entryCount, sizeof(entry));
|
||||||
r = archive_map_entries_index(ctx);
|
r = archive_map_entries_index(ctx, sort_entries);
|
||||||
if (!archive_prealloc_mempool()) {
|
if (!archive_prealloc_mempool()) {
|
||||||
r = 0;
|
r = 0;
|
||||||
for (int i = 0; i < entryCount; ++i) {
|
for (int i = 0; i < entryCount; ++i) {
|
||||||
@@ -354,40 +358,47 @@ Java_com_hippo_ehviewer_jni_ArchiveKt_openArchive(JNIEnv *env, jclass thiz, jint
|
|||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
|
|
||||||
#pragma clang diagnostic push
|
|
||||||
#pragma ide diagnostic ignored "ConstantConditionsOC"
|
|
||||||
#pragma ide diagnostic ignored "UnreachableCode"
|
|
||||||
|
|
||||||
JNIEXPORT jobject JNICALL
|
JNIEXPORT jobject JNICALL
|
||||||
Java_com_hippo_ehviewer_jni_ArchiveKt_extractToByteBuffer(JNIEnv *env, jclass thiz,
|
Java_com_hippo_ehviewer_jni_ArchiveKt_extractToByteBuffer(JNIEnv *env, jclass thiz, jint index) {
|
||||||
jint index) {
|
|
||||||
EH_UNUSED(env);
|
EH_UNUSED(env);
|
||||||
EH_UNUSED(thiz);
|
EH_UNUSED(thiz);
|
||||||
void *addr = MEMPOOL_ADDR_BY_SORTED_IDX(index);
|
void *addr = MEMPOOL_ADDR_BY_SORTED_IDX(index);
|
||||||
size_t size = entries[index].size;
|
size_t size = entries[index].size;
|
||||||
if (!MEMPOOL_IN_CORE_AND_LOCK(index)) {
|
index = entries[index].index;
|
||||||
index = entries[index].index;
|
archive_ctx *ctx = NULL;
|
||||||
archive_ctx *ctx = NULL;
|
int ret = archive_get_ctx(&ctx, index);
|
||||||
int ret;
|
if (ret) return 0;
|
||||||
ret = archive_get_ctx(&ctx, index);
|
void *buffer = NULL;
|
||||||
if (ret) return 0;
|
size_t buffer_size = 0;
|
||||||
ret = archive_read_data(ctx->arc, addr, size);
|
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;
|
ctx->using = 0;
|
||||||
if (ret == size)
|
return (*env)->NewDirectByteBuffer(env, buffer, buffer_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);
|
|
||||||
} else {
|
} 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;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#pragma clang diagnostic pop
|
|
||||||
|
|
||||||
JNIEXPORT void JNICALL
|
JNIEXPORT void JNICALL
|
||||||
Java_com_hippo_ehviewer_jni_ArchiveKt_closeArchive(JNIEnv *env, jclass thiz) {
|
Java_com_hippo_ehviewer_jni_ArchiveKt_closeArchive(JNIEnv *env, jclass thiz) {
|
||||||
EH_UNUSED(env);
|
EH_UNUSED(env);
|
||||||
@@ -415,8 +426,7 @@ Java_com_hippo_ehviewer_jni_ArchiveKt_needPassword(JNIEnv *env, jclass thiz) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
JNIEXPORT jboolean JNICALL
|
JNIEXPORT jboolean JNICALL
|
||||||
Java_com_hippo_ehviewer_jni_ArchiveKt_providePassword(JNIEnv *env, jclass thiz,
|
Java_com_hippo_ehviewer_jni_ArchiveKt_providePassword(JNIEnv *env, jclass thiz, jstring str) {
|
||||||
jstring str) {
|
|
||||||
EH_UNUSED(thiz);
|
EH_UNUSED(thiz);
|
||||||
struct archive_entry *entry;
|
struct archive_entry *entry;
|
||||||
archive_ctx *ctx;
|
archive_ctx *ctx;
|
||||||
@@ -427,11 +437,11 @@ Java_com_hippo_ehviewer_jni_ArchiveKt_providePassword(JNIEnv *env, jclass thiz,
|
|||||||
LOGE("Allocate passwd buffer failed");
|
LOGE("Allocate passwd buffer failed");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
strcpy(passwd, (*env)->GetStringUTFChars(env, str, NULL));
|
(*env)->GetStringUTFRegion(env, str, 0, len, passwd);
|
||||||
archive_alloc_ctx(&ctx);
|
archive_alloc_ctx(&ctx);
|
||||||
void *tmpBuf = alloca(4096);
|
void *tmpBuf = alloca(4096);
|
||||||
while (archive_read_next_header(ctx->arc, &entry) == ARCHIVE_OK) {
|
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;
|
continue;
|
||||||
if (!archive_entry_is_encrypted(entry))
|
if (!archive_entry_is_encrypted(entry))
|
||||||
continue;
|
continue;
|
||||||
@@ -446,24 +456,15 @@ Java_com_hippo_ehviewer_jni_ArchiveKt_providePassword(JNIEnv *env, jclass thiz,
|
|||||||
}
|
}
|
||||||
|
|
||||||
JNIEXPORT jstring JNICALL
|
JNIEXPORT jstring JNICALL
|
||||||
Java_com_hippo_ehviewer_jni_ArchiveKt_getFilename(JNIEnv *env, jclass thiz,
|
Java_com_hippo_ehviewer_jni_ArchiveKt_getExtension(JNIEnv *env, jclass thiz, jint index) {
|
||||||
jint index) {
|
|
||||||
EH_UNUSED(env);
|
EH_UNUSED(env);
|
||||||
EH_UNUSED(thiz);
|
EH_UNUSED(thiz);
|
||||||
index = entries[index].index;
|
const char *ext = strrchr(entries[index].filename, '.') + 1;
|
||||||
archive_ctx *ctx = NULL;
|
return (*env)->NewStringUTF(env, ext);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
JNIEXPORT void JNICALL
|
JNIEXPORT jboolean JNICALL
|
||||||
Java_com_hippo_ehviewer_jni_ArchiveKt_extractToFd(JNIEnv *env, jclass thiz,
|
Java_com_hippo_ehviewer_jni_ArchiveKt_extractToFd(JNIEnv *env, jclass thiz, jint index, jint fd) {
|
||||||
jint index, jint fd) {
|
|
||||||
EH_UNUSED(env);
|
EH_UNUSED(env);
|
||||||
EH_UNUSED(thiz);
|
EH_UNUSED(thiz);
|
||||||
index = entries[index].index;
|
index = entries[index].index;
|
||||||
@@ -471,16 +472,51 @@ Java_com_hippo_ehviewer_jni_ArchiveKt_extractToFd(JNIEnv *env, jclass thiz,
|
|||||||
int ret;
|
int ret;
|
||||||
ret = archive_get_ctx(&ctx, index);
|
ret = archive_get_ctx(&ctx, index);
|
||||||
if (!ret) {
|
if (!ret) {
|
||||||
archive_read_data_into_fd(ctx->arc, fd);
|
ret = archive_read_data_into_fd(ctx->arc, fd);
|
||||||
ctx->using = 0;
|
ctx->using = 0;
|
||||||
}
|
}
|
||||||
|
return ret == ARCHIVE_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
JNIEXPORT void JNICALL
|
JNIEXPORT void JNICALL
|
||||||
Java_com_hippo_ehviewer_jni_ArchiveKt_releaseByteBuffer(JNIEnv *env, jclass thiz,
|
Java_com_hippo_ehviewer_jni_ArchiveKt_releaseByteBuffer(JNIEnv *env, jclass thiz, jobject buffer) {
|
||||||
jobject buffer) {
|
|
||||||
EH_UNUSED(thiz);
|
EH_UNUSED(thiz);
|
||||||
void *addr = (*env)->GetDirectBufferAddress(env, buffer);
|
void *addr = (*env)->GetDirectBufferAddress(env, buffer);
|
||||||
size_t size = (*env)->GetDirectBufferCapacity(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;
|
||||||
|
}
|
||||||
@@ -84,30 +84,6 @@ compare_right(nat_char const *a, nat_char const *b)
|
|||||||
} else if (!*a && !*b)
|
} else if (!*a && !*b)
|
||||||
return bias;
|
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;
|
int ai, bi;
|
||||||
nat_char ca, cb;
|
nat_char ca, cb;
|
||||||
int fractional, result;
|
int result;
|
||||||
|
|
||||||
ai = bi = 0;
|
ai = bi = 0;
|
||||||
while (1) {
|
while (1) {
|
||||||
ca = a[ai]; cb = b[bi];
|
ca = a[ai]; cb = b[bi];
|
||||||
|
|
||||||
/* skip over leading spaces or zeros */
|
/* skip over leading spaces or zeros */
|
||||||
while (nat_isspace(ca))
|
while (nat_isspace(ca) || ca == '0')
|
||||||
ca = a[++ai];
|
ca = a[++ai];
|
||||||
|
|
||||||
while (nat_isspace(cb))
|
while (nat_isspace(cb) || cb == '0')
|
||||||
cb = b[++bi];
|
cb = b[++bi];
|
||||||
|
|
||||||
/* process run of digits */
|
/* process run of digits */
|
||||||
if (nat_isdigit(ca) && nat_isdigit(cb)) {
|
if (nat_isdigit(ca) && nat_isdigit(cb)) {
|
||||||
fractional = (ca == '0' || cb == '0');
|
if ((result = compare_right(a+ai, b+bi)) != 0)
|
||||||
|
return result;
|
||||||
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 (!ca && !cb) {
|
if (!ca && !cb) {
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
/nettle
|
|
||||||
@@ -5,232 +5,232 @@ set(LIBNETTLE_DEFINITIONS
|
|||||||
-DHAVE_CONFIG_H)
|
-DHAVE_CONFIG_H)
|
||||||
|
|
||||||
set(LIBNETTLE_SOURCES
|
set(LIBNETTLE_SOURCES
|
||||||
nettle/aes-decrypt-internal.c
|
${NETTLE_DIR}/aes-decrypt-internal.c
|
||||||
nettle/aes-decrypt.c
|
${NETTLE_DIR}/aes-decrypt.c
|
||||||
nettle/aes-decrypt-table.c
|
${NETTLE_DIR}/aes-decrypt-table.c
|
||||||
nettle/aes128-decrypt.c
|
${NETTLE_DIR}/aes128-decrypt.c
|
||||||
nettle/aes192-decrypt.c
|
${NETTLE_DIR}/aes192-decrypt.c
|
||||||
nettle/aes256-decrypt.c
|
${NETTLE_DIR}/aes256-decrypt.c
|
||||||
nettle/aes-encrypt-internal.c
|
${NETTLE_DIR}/aes-encrypt-internal.c
|
||||||
nettle/aes-encrypt.c
|
${NETTLE_DIR}/aes-encrypt.c
|
||||||
nettle/aes-encrypt-table.c
|
${NETTLE_DIR}/aes-encrypt-table.c
|
||||||
nettle/aes128-encrypt.c
|
${NETTLE_DIR}/aes128-encrypt.c
|
||||||
nettle/aes192-encrypt.c
|
${NETTLE_DIR}/aes192-encrypt.c
|
||||||
nettle/aes256-encrypt.c
|
${NETTLE_DIR}/aes256-encrypt.c
|
||||||
nettle/aes-invert-internal.c
|
${NETTLE_DIR}/aes-invert-internal.c
|
||||||
nettle/aes-set-key-internal.c
|
${NETTLE_DIR}/aes-set-key-internal.c
|
||||||
nettle/aes-set-encrypt-key.c
|
${NETTLE_DIR}/aes-set-encrypt-key.c
|
||||||
nettle/aes-set-decrypt-key.c
|
${NETTLE_DIR}/aes-set-decrypt-key.c
|
||||||
nettle/aes128-set-encrypt-key.c
|
${NETTLE_DIR}/aes128-set-encrypt-key.c
|
||||||
nettle/aes128-set-decrypt-key.c
|
${NETTLE_DIR}/aes128-set-decrypt-key.c
|
||||||
nettle/aes128-meta.c
|
${NETTLE_DIR}/aes128-meta.c
|
||||||
nettle/aes192-set-encrypt-key.c
|
${NETTLE_DIR}/aes192-set-encrypt-key.c
|
||||||
nettle/aes192-set-decrypt-key.c
|
${NETTLE_DIR}/aes192-set-decrypt-key.c
|
||||||
nettle/aes192-meta.c
|
${NETTLE_DIR}/aes192-meta.c
|
||||||
nettle/aes256-set-encrypt-key.c
|
${NETTLE_DIR}/aes256-set-encrypt-key.c
|
||||||
nettle/aes256-set-decrypt-key.c
|
${NETTLE_DIR}/aes256-set-decrypt-key.c
|
||||||
nettle/aes256-meta.c
|
${NETTLE_DIR}/aes256-meta.c
|
||||||
nettle/nist-keywrap.c
|
${NETTLE_DIR}/nist-keywrap.c
|
||||||
nettle/arcfour.c
|
${NETTLE_DIR}/arcfour.c
|
||||||
nettle/arctwo.c
|
${NETTLE_DIR}/arctwo.c
|
||||||
nettle/arctwo-meta.c
|
${NETTLE_DIR}/arctwo-meta.c
|
||||||
nettle/blowfish.c
|
${NETTLE_DIR}/blowfish.c
|
||||||
nettle/blowfish-bcrypt.c
|
${NETTLE_DIR}/blowfish-bcrypt.c
|
||||||
nettle/base16-encode.c
|
${NETTLE_DIR}/base16-encode.c
|
||||||
nettle/base16-decode.c
|
${NETTLE_DIR}/base16-decode.c
|
||||||
nettle/base16-meta.c
|
${NETTLE_DIR}/base16-meta.c
|
||||||
nettle/base64-encode.c
|
${NETTLE_DIR}/base64-encode.c
|
||||||
nettle/base64-decode.c
|
${NETTLE_DIR}/base64-decode.c
|
||||||
nettle/base64-meta.c
|
${NETTLE_DIR}/base64-meta.c
|
||||||
nettle/base64url-encode.c
|
${NETTLE_DIR}/base64url-encode.c
|
||||||
nettle/base64url-decode.c
|
${NETTLE_DIR}/base64url-decode.c
|
||||||
nettle/base64url-meta.c
|
${NETTLE_DIR}/base64url-meta.c
|
||||||
nettle/buffer.c
|
${NETTLE_DIR}/buffer.c
|
||||||
nettle/buffer-init.c
|
${NETTLE_DIR}/buffer-init.c
|
||||||
nettle/camellia-crypt-internal.c
|
${NETTLE_DIR}/camellia-crypt-internal.c
|
||||||
nettle/camellia-table.c
|
${NETTLE_DIR}/camellia-table.c
|
||||||
nettle/camellia-absorb.c
|
${NETTLE_DIR}/camellia-absorb.c
|
||||||
nettle/camellia-invert-key.c
|
${NETTLE_DIR}/camellia-invert-key.c
|
||||||
nettle/camellia128-set-encrypt-key.c
|
${NETTLE_DIR}/camellia128-set-encrypt-key.c
|
||||||
nettle/camellia128-crypt.c
|
${NETTLE_DIR}/camellia128-crypt.c
|
||||||
nettle/camellia128-set-decrypt-key.c
|
${NETTLE_DIR}/camellia128-set-decrypt-key.c
|
||||||
nettle/camellia128-meta.c
|
${NETTLE_DIR}/camellia128-meta.c
|
||||||
nettle/camellia192-meta.c
|
${NETTLE_DIR}/camellia192-meta.c
|
||||||
nettle/camellia256-set-encrypt-key.c
|
${NETTLE_DIR}/camellia256-set-encrypt-key.c
|
||||||
nettle/camellia256-crypt.c
|
${NETTLE_DIR}/camellia256-crypt.c
|
||||||
nettle/camellia256-set-decrypt-key.c
|
${NETTLE_DIR}/camellia256-set-decrypt-key.c
|
||||||
nettle/camellia256-meta.c
|
${NETTLE_DIR}/camellia256-meta.c
|
||||||
nettle/cast128.c
|
${NETTLE_DIR}/cast128.c
|
||||||
nettle/cast128-meta.c
|
${NETTLE_DIR}/cast128-meta.c
|
||||||
nettle/cbc.c
|
${NETTLE_DIR}/cbc.c
|
||||||
nettle/cbc-aes128-encrypt.c
|
${NETTLE_DIR}/cbc-aes128-encrypt.c
|
||||||
nettle/cbc-aes192-encrypt.c
|
${NETTLE_DIR}/cbc-aes192-encrypt.c
|
||||||
nettle/cbc-aes256-encrypt.c
|
${NETTLE_DIR}/cbc-aes256-encrypt.c
|
||||||
nettle/ccm.c
|
${NETTLE_DIR}/ccm.c
|
||||||
nettle/ccm-aes128.c
|
${NETTLE_DIR}/ccm-aes128.c
|
||||||
nettle/ccm-aes192.c
|
${NETTLE_DIR}/ccm-aes192.c
|
||||||
nettle/ccm-aes256.c
|
${NETTLE_DIR}/ccm-aes256.c
|
||||||
nettle/cfb.c
|
${NETTLE_DIR}/cfb.c
|
||||||
nettle/siv-cmac.c
|
${NETTLE_DIR}/siv-cmac.c
|
||||||
nettle/siv-cmac-aes128.c
|
${NETTLE_DIR}/siv-cmac-aes128.c
|
||||||
nettle/siv-cmac-aes256.c
|
${NETTLE_DIR}/siv-cmac-aes256.c
|
||||||
nettle/cnd-memcpy.c
|
${NETTLE_DIR}/cnd-memcpy.c
|
||||||
nettle/chacha-crypt.c
|
${NETTLE_DIR}/chacha-crypt.c
|
||||||
nettle/chacha-core-internal.c
|
${NETTLE_DIR}/chacha-core-internal.c
|
||||||
nettle/chacha-poly1305.c
|
${NETTLE_DIR}/chacha-poly1305.c
|
||||||
nettle/chacha-poly1305-meta.c
|
${NETTLE_DIR}/chacha-poly1305-meta.c
|
||||||
nettle/chacha-set-key.c
|
${NETTLE_DIR}/chacha-set-key.c
|
||||||
nettle/chacha-set-nonce.c
|
${NETTLE_DIR}/chacha-set-nonce.c
|
||||||
nettle/ctr.c
|
${NETTLE_DIR}/ctr.c
|
||||||
nettle/ctr16.c
|
${NETTLE_DIR}/ctr16.c
|
||||||
nettle/des.c
|
${NETTLE_DIR}/des.c
|
||||||
nettle/des3.c
|
${NETTLE_DIR}/des3.c
|
||||||
nettle/eax.c
|
${NETTLE_DIR}/eax.c
|
||||||
nettle/eax-aes128.c
|
${NETTLE_DIR}/eax-aes128.c
|
||||||
nettle/eax-aes128-meta.c
|
${NETTLE_DIR}/eax-aes128-meta.c
|
||||||
nettle/ghash-set-key.c
|
${NETTLE_DIR}/ghash-set-key.c
|
||||||
nettle/ghash-update.c
|
${NETTLE_DIR}/ghash-update.c
|
||||||
nettle/gcm.c
|
${NETTLE_DIR}/gcm.c
|
||||||
nettle/gcm-aes.c
|
${NETTLE_DIR}/gcm-aes.c
|
||||||
nettle/gcm-aes128.c
|
${NETTLE_DIR}/gcm-aes128.c
|
||||||
nettle/gcm-aes128-meta.c
|
${NETTLE_DIR}/gcm-aes128-meta.c
|
||||||
nettle/gcm-aes192.c
|
${NETTLE_DIR}/gcm-aes192.c
|
||||||
nettle/gcm-aes192-meta.c
|
${NETTLE_DIR}/gcm-aes192-meta.c
|
||||||
nettle/gcm-aes256.c
|
${NETTLE_DIR}/gcm-aes256.c
|
||||||
nettle/gcm-aes256-meta.c
|
${NETTLE_DIR}/gcm-aes256-meta.c
|
||||||
nettle/gcm-camellia128.c
|
${NETTLE_DIR}/gcm-camellia128.c
|
||||||
nettle/gcm-camellia128-meta.c
|
${NETTLE_DIR}/gcm-camellia128-meta.c
|
||||||
nettle/gcm-camellia256.c
|
${NETTLE_DIR}/gcm-camellia256.c
|
||||||
nettle/gcm-camellia256-meta.c
|
${NETTLE_DIR}/gcm-camellia256-meta.c
|
||||||
nettle/cmac.c
|
${NETTLE_DIR}/cmac.c
|
||||||
nettle/cmac64.c
|
${NETTLE_DIR}/cmac64.c
|
||||||
nettle/cmac-aes128.c
|
${NETTLE_DIR}/cmac-aes128.c
|
||||||
nettle/cmac-aes256.c
|
${NETTLE_DIR}/cmac-aes256.c
|
||||||
nettle/cmac-des3.c
|
${NETTLE_DIR}/cmac-des3.c
|
||||||
nettle/cmac-aes128-meta.c
|
${NETTLE_DIR}/cmac-aes128-meta.c
|
||||||
nettle/cmac-aes256-meta.c
|
${NETTLE_DIR}/cmac-aes256-meta.c
|
||||||
nettle/cmac-des3-meta.c
|
${NETTLE_DIR}/cmac-des3-meta.c
|
||||||
nettle/gost28147.c
|
${NETTLE_DIR}/gost28147.c
|
||||||
nettle/gosthash94.c
|
${NETTLE_DIR}/gosthash94.c
|
||||||
nettle/gosthash94-meta.c
|
${NETTLE_DIR}/gosthash94-meta.c
|
||||||
nettle/hmac.c
|
${NETTLE_DIR}/hmac.c
|
||||||
nettle/hmac-gosthash94.c
|
${NETTLE_DIR}/hmac-gosthash94.c
|
||||||
nettle/hmac-md5.c
|
${NETTLE_DIR}/hmac-md5.c
|
||||||
nettle/hmac-ripemd160.c
|
${NETTLE_DIR}/hmac-ripemd160.c
|
||||||
nettle/hmac-sha1.c
|
${NETTLE_DIR}/hmac-sha1.c
|
||||||
nettle/hmac-sha224.c
|
${NETTLE_DIR}/hmac-sha224.c
|
||||||
nettle/hmac-sha256.c
|
${NETTLE_DIR}/hmac-sha256.c
|
||||||
nettle/hmac-sha384.c
|
${NETTLE_DIR}/hmac-sha384.c
|
||||||
nettle/hmac-sha512.c
|
${NETTLE_DIR}/hmac-sha512.c
|
||||||
nettle/hmac-streebog.c
|
${NETTLE_DIR}/hmac-streebog.c
|
||||||
nettle/hmac-sm3.c
|
${NETTLE_DIR}/hmac-sm3.c
|
||||||
nettle/hmac-md5-meta.c
|
${NETTLE_DIR}/hmac-md5-meta.c
|
||||||
nettle/hmac-ripemd160-meta.c
|
${NETTLE_DIR}/hmac-ripemd160-meta.c
|
||||||
nettle/hmac-sha1-meta.c
|
${NETTLE_DIR}/hmac-sha1-meta.c
|
||||||
nettle/hmac-sha224-meta.c
|
${NETTLE_DIR}/hmac-sha224-meta.c
|
||||||
nettle/hmac-sha256-meta.c
|
${NETTLE_DIR}/hmac-sha256-meta.c
|
||||||
nettle/hmac-sha384-meta.c
|
${NETTLE_DIR}/hmac-sha384-meta.c
|
||||||
nettle/hmac-sha512-meta.c
|
${NETTLE_DIR}/hmac-sha512-meta.c
|
||||||
nettle/hmac-streebog-meta.c
|
${NETTLE_DIR}/hmac-streebog-meta.c
|
||||||
nettle/hmac-sm3-meta.c
|
${NETTLE_DIR}/hmac-sm3-meta.c
|
||||||
nettle/knuth-lfib.c
|
${NETTLE_DIR}/knuth-lfib.c
|
||||||
nettle/hkdf.c
|
${NETTLE_DIR}/hkdf.c
|
||||||
nettle/md2.c
|
${NETTLE_DIR}/md2.c
|
||||||
nettle/md2-meta.c
|
${NETTLE_DIR}/md2-meta.c
|
||||||
nettle/md4.c
|
${NETTLE_DIR}/md4.c
|
||||||
nettle/md4-meta.c
|
${NETTLE_DIR}/md4-meta.c
|
||||||
nettle/md5.c
|
${NETTLE_DIR}/md5.c
|
||||||
nettle/md5-compress.c
|
${NETTLE_DIR}/md5-compress.c
|
||||||
nettle/md5-compat.c
|
${NETTLE_DIR}/md5-compat.c
|
||||||
nettle/md5-meta.c
|
${NETTLE_DIR}/md5-meta.c
|
||||||
nettle/memeql-sec.c
|
${NETTLE_DIR}/memeql-sec.c
|
||||||
nettle/memxor.c
|
${NETTLE_DIR}/memxor.c
|
||||||
nettle/memxor3.c
|
${NETTLE_DIR}/memxor3.c
|
||||||
nettle/nettle-lookup-hash.c
|
${NETTLE_DIR}/nettle-lookup-hash.c
|
||||||
nettle/nettle-meta-aeads.c
|
${NETTLE_DIR}/nettle-meta-aeads.c
|
||||||
nettle/nettle-meta-armors.c
|
${NETTLE_DIR}/nettle-meta-armors.c
|
||||||
nettle/nettle-meta-ciphers.c
|
${NETTLE_DIR}/nettle-meta-ciphers.c
|
||||||
nettle/nettle-meta-hashes.c
|
${NETTLE_DIR}/nettle-meta-hashes.c
|
||||||
nettle/nettle-meta-macs.c
|
${NETTLE_DIR}/nettle-meta-macs.c
|
||||||
nettle/pbkdf2.c
|
${NETTLE_DIR}/pbkdf2.c
|
||||||
nettle/pbkdf2-hmac-gosthash94.c
|
${NETTLE_DIR}/pbkdf2-hmac-gosthash94.c
|
||||||
nettle/pbkdf2-hmac-sha1.c
|
${NETTLE_DIR}/pbkdf2-hmac-sha1.c
|
||||||
nettle/pbkdf2-hmac-sha256.c
|
${NETTLE_DIR}/pbkdf2-hmac-sha256.c
|
||||||
nettle/pbkdf2-hmac-sha384.c
|
${NETTLE_DIR}/pbkdf2-hmac-sha384.c
|
||||||
nettle/pbkdf2-hmac-sha512.c
|
${NETTLE_DIR}/pbkdf2-hmac-sha512.c
|
||||||
nettle/poly1305-aes.c
|
${NETTLE_DIR}/poly1305-aes.c
|
||||||
nettle/poly1305-internal.c
|
${NETTLE_DIR}/poly1305-internal.c
|
||||||
nettle/realloc.c
|
${NETTLE_DIR}/realloc.c
|
||||||
nettle/ripemd160.c
|
${NETTLE_DIR}/ripemd160.c
|
||||||
nettle/ripemd160-compress.c
|
${NETTLE_DIR}/ripemd160-compress.c
|
||||||
nettle/ripemd160-meta.c
|
${NETTLE_DIR}/ripemd160-meta.c
|
||||||
nettle/salsa20-core-internal.c
|
${NETTLE_DIR}/salsa20-core-internal.c
|
||||||
nettle/salsa20-crypt-internal.c
|
${NETTLE_DIR}/salsa20-crypt-internal.c
|
||||||
nettle/salsa20-crypt.c
|
${NETTLE_DIR}/salsa20-crypt.c
|
||||||
nettle/salsa20r12-crypt.c
|
${NETTLE_DIR}/salsa20r12-crypt.c
|
||||||
nettle/salsa20-set-key.c
|
${NETTLE_DIR}/salsa20-set-key.c
|
||||||
nettle/salsa20-set-nonce.c
|
${NETTLE_DIR}/salsa20-set-nonce.c
|
||||||
nettle/salsa20-128-set-key.c
|
${NETTLE_DIR}/salsa20-128-set-key.c
|
||||||
nettle/salsa20-256-set-key.c
|
${NETTLE_DIR}/salsa20-256-set-key.c
|
||||||
nettle/sha1.c
|
${NETTLE_DIR}/sha1.c
|
||||||
nettle/sha1-compress.c
|
${NETTLE_DIR}/sha1-compress.c
|
||||||
nettle/sha1-meta.c
|
${NETTLE_DIR}/sha1-meta.c
|
||||||
nettle/sha256.c
|
${NETTLE_DIR}/sha256.c
|
||||||
nettle/sha256-compress-n.c
|
${NETTLE_DIR}/sha256-compress-n.c
|
||||||
nettle/sha224-meta.c
|
${NETTLE_DIR}/sha224-meta.c
|
||||||
nettle/sha256-meta.c
|
${NETTLE_DIR}/sha256-meta.c
|
||||||
nettle/sha512.c
|
${NETTLE_DIR}/sha512.c
|
||||||
nettle/sha512-compress.c
|
${NETTLE_DIR}/sha512-compress.c
|
||||||
nettle/sha384-meta.c
|
${NETTLE_DIR}/sha384-meta.c
|
||||||
nettle/sha512-meta.c
|
${NETTLE_DIR}/sha512-meta.c
|
||||||
nettle/sha512-224-meta.c
|
${NETTLE_DIR}/sha512-224-meta.c
|
||||||
nettle/sha512-256-meta.c
|
${NETTLE_DIR}/sha512-256-meta.c
|
||||||
nettle/sha3.c
|
${NETTLE_DIR}/sha3.c
|
||||||
nettle/sha3-permute.c
|
${NETTLE_DIR}/sha3-permute.c
|
||||||
nettle/sha3-224.c
|
${NETTLE_DIR}/sha3-224.c
|
||||||
nettle/sha3-224-meta.c
|
${NETTLE_DIR}/sha3-224-meta.c
|
||||||
nettle/sha3-256.c
|
${NETTLE_DIR}/sha3-256.c
|
||||||
nettle/sha3-256-meta.c
|
${NETTLE_DIR}/sha3-256-meta.c
|
||||||
nettle/sha3-384.c
|
${NETTLE_DIR}/sha3-384.c
|
||||||
nettle/sha3-384-meta.c
|
${NETTLE_DIR}/sha3-384-meta.c
|
||||||
nettle/sha3-512.c
|
${NETTLE_DIR}/sha3-512.c
|
||||||
nettle/sha3-512-meta.c
|
${NETTLE_DIR}/sha3-512-meta.c
|
||||||
nettle/shake256.c
|
${NETTLE_DIR}/shake256.c
|
||||||
nettle/sm3.c
|
${NETTLE_DIR}/sm3.c
|
||||||
nettle/sm3-meta.c
|
${NETTLE_DIR}/sm3-meta.c
|
||||||
nettle/serpent-set-key.c
|
${NETTLE_DIR}/serpent-set-key.c
|
||||||
nettle/serpent-encrypt.c
|
${NETTLE_DIR}/serpent-encrypt.c
|
||||||
nettle/serpent-decrypt.c
|
${NETTLE_DIR}/serpent-decrypt.c
|
||||||
nettle/serpent-meta.c
|
${NETTLE_DIR}/serpent-meta.c
|
||||||
nettle/streebog.c
|
${NETTLE_DIR}/streebog.c
|
||||||
nettle/streebog-meta.c
|
${NETTLE_DIR}/streebog-meta.c
|
||||||
nettle/twofish.c
|
${NETTLE_DIR}/twofish.c
|
||||||
nettle/twofish-meta.c
|
${NETTLE_DIR}/twofish-meta.c
|
||||||
nettle/umac-nh.c
|
${NETTLE_DIR}/umac-nh.c
|
||||||
nettle/umac-nh-n.c
|
${NETTLE_DIR}/umac-nh-n.c
|
||||||
nettle/umac-l2.c
|
${NETTLE_DIR}/umac-l2.c
|
||||||
nettle/umac-l3.c
|
${NETTLE_DIR}/umac-l3.c
|
||||||
nettle/umac-poly64.c
|
${NETTLE_DIR}/umac-poly64.c
|
||||||
nettle/umac-poly128.c
|
${NETTLE_DIR}/umac-poly128.c
|
||||||
nettle/umac-set-key.c
|
${NETTLE_DIR}/umac-set-key.c
|
||||||
nettle/umac32.c
|
${NETTLE_DIR}/umac32.c
|
||||||
nettle/umac64.c
|
${NETTLE_DIR}/umac64.c
|
||||||
nettle/umac96.c
|
${NETTLE_DIR}/umac96.c
|
||||||
nettle/umac128.c
|
${NETTLE_DIR}/umac128.c
|
||||||
nettle/version.c
|
${NETTLE_DIR}/version.c
|
||||||
nettle/write-be32.c
|
${NETTLE_DIR}/write-be32.c
|
||||||
nettle/write-le32.c
|
${NETTLE_DIR}/write-le32.c
|
||||||
nettle/write-le64.c
|
${NETTLE_DIR}/write-le64.c
|
||||||
nettle/yarrow256.c
|
${NETTLE_DIR}/yarrow256.c
|
||||||
nettle/yarrow_key_event.c
|
${NETTLE_DIR}/yarrow_key_event.c
|
||||||
nettle/xts.c
|
${NETTLE_DIR}/xts.c
|
||||||
nettle/xts-aes128.c
|
${NETTLE_DIR}/xts-aes128.c
|
||||||
nettle/xts-aes256.c
|
${NETTLE_DIR}/xts-aes256.c
|
||||||
)
|
)
|
||||||
|
|
||||||
set(LIBNETTLE_INCLUDES
|
set(LIBNETTLE_INCLUDES
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}
|
${CMAKE_CURRENT_SOURCE_DIR}
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/nettle
|
${NETTLE_DIR}
|
||||||
)
|
)
|
||||||
|
|
||||||
add_library(nettle STATIC ${LIBNETTLE_SOURCES})
|
add_library(nettle STATIC ${LIBNETTLE_SOURCES})
|
||||||
|
|||||||
@@ -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
|
|
||||||
+278
@@ -0,0 +1,278 @@
|
|||||||
|
/*
|
||||||
|
* 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.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.gestures.HorizontalDragEvent.DragDelta
|
||||||
|
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.requireLayoutDirection
|
||||||
|
import androidx.compose.ui.platform.InspectorInfo
|
||||||
|
import androidx.compose.ui.platform.debugInspectorInfo
|
||||||
|
import androidx.compose.ui.unit.Constraints
|
||||||
|
import androidx.compose.ui.unit.IntSize
|
||||||
|
import androidx.compose.ui.unit.LayoutDirection
|
||||||
|
import androidx.compose.ui.unit.Velocity
|
||||||
|
import kotlin.math.absoluteValue
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
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,
|
||||||
|
): Modifier = this then AnchoredHorizontalDraggableElement(
|
||||||
|
state = state,
|
||||||
|
canDrag = { (x, y) ->
|
||||||
|
if (x > 0) {
|
||||||
|
enableDragFromStartToEnd
|
||||||
|
} else {
|
||||||
|
enableDragFromEndToStart
|
||||||
|
} && x.absoluteValue > y.absoluteValue * 2f
|
||||||
|
},
|
||||||
|
enabled = enabled,
|
||||||
|
startDragImmediately = startDragImmediately,
|
||||||
|
)
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
|
private class AnchoredHorizontalDraggableElement<T>(
|
||||||
|
private val state: AnchoredDraggableState<T>,
|
||||||
|
private val canDrag: (Offset) -> Boolean,
|
||||||
|
private val enabled: Boolean,
|
||||||
|
private val startDragImmediately: Boolean,
|
||||||
|
) : ModifierNodeElement<AnchoredHorizontalDraggableNode<T>>() {
|
||||||
|
override fun create() = AnchoredHorizontalDraggableNode(
|
||||||
|
state,
|
||||||
|
canDrag,
|
||||||
|
enabled,
|
||||||
|
startDragImmediately,
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun update(node: AnchoredHorizontalDraggableNode<T>) {
|
||||||
|
node.update(
|
||||||
|
state,
|
||||||
|
canDrag,
|
||||||
|
enabled,
|
||||||
|
startDragImmediately,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = state.hashCode()
|
||||||
|
result = 31 * result + canDrag.hashCode()
|
||||||
|
result = 31 * result + enabled.hashCode()
|
||||||
|
result = 31 * result + startDragImmediately.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
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun InspectorInfo.inspectableProperties() {
|
||||||
|
name = "anchoredDraggable"
|
||||||
|
properties["state"] = state
|
||||||
|
properties["canDrag"] = canDrag
|
||||||
|
properties["enabled"] = enabled
|
||||||
|
properties["startDragImmediately"] = startDragImmediately
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
|
private class AnchoredHorizontalDraggableNode<T>(
|
||||||
|
private var state: AnchoredDraggableState<T>,
|
||||||
|
private var canDrag: (Offset) -> Boolean,
|
||||||
|
enabled: Boolean,
|
||||||
|
private var startDragImmediately: Boolean,
|
||||||
|
) : HorizontalDragGestureNode(
|
||||||
|
enabled = enabled,
|
||||||
|
) {
|
||||||
|
|
||||||
|
private val isReverseDirection: Boolean
|
||||||
|
get() = requireLayoutDirection() == LayoutDirection.Rtl
|
||||||
|
|
||||||
|
override fun canDrag(change: PointerInputChange): Boolean {
|
||||||
|
return 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 {
|
||||||
|
state.settle(velocity.reverseIfNeeded().x)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun startDragImmediately(): Boolean = startDragImmediately
|
||||||
|
|
||||||
|
fun update(
|
||||||
|
state: AnchoredDraggableState<T>,
|
||||||
|
canDrag: (Offset) -> Boolean,
|
||||||
|
enabled: Boolean,
|
||||||
|
startDragImmediately: Boolean,
|
||||||
|
) {
|
||||||
|
var resetPointerInputHandling = false
|
||||||
|
|
||||||
|
if (this.state != state) {
|
||||||
|
this.state = state
|
||||||
|
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.minAnchor(), anchors.maxAnchor())
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 {
|
||||||
|
return 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()
|
||||||
|
}
|
||||||
@@ -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.contentPaddingWithoutLabel(),
|
||||||
|
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
|
||||||
@@ -0,0 +1,367 @@
|
|||||||
|
@file:Suppress("ktlint:standard:property-naming", "ktlint:standard:function-naming")
|
||||||
|
|
||||||
|
package androidx.compose.material3
|
||||||
|
|
||||||
|
import androidx.compose.animation.core.AnimationSpec
|
||||||
|
import androidx.compose.animation.core.TweenSpec
|
||||||
|
import androidx.compose.animation.core.animate
|
||||||
|
import androidx.compose.animation.core.exponentialDecay
|
||||||
|
import androidx.compose.foundation.Canvas
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.gestures.AnchoredDraggableState
|
||||||
|
import androidx.compose.foundation.gestures.DraggableAnchors
|
||||||
|
import androidx.compose.foundation.gestures.anchoredHorizontalDraggable
|
||||||
|
import androidx.compose.foundation.gestures.detectTapGestures
|
||||||
|
import androidx.compose.foundation.gestures.snapTo
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.offset
|
||||||
|
import androidx.compose.foundation.shape.CornerSize
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.SideEffect
|
||||||
|
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
|
||||||
|
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.graphics.BlurEffect
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
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.layout.layout
|
||||||
|
import androidx.compose.ui.layout.onSizeChanged
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.unit.Constraints
|
||||||
|
import androidx.compose.ui.unit.Density
|
||||||
|
import androidx.compose.ui.unit.IntOffset
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.util.lerp
|
||||||
|
import com.hippo.ehviewer.ui.tools.animateFloatMergeOneWayPredictiveBackAsState
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
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
|
||||||
|
|
||||||
|
@Suppress("NotCloseable")
|
||||||
|
@Stable
|
||||||
|
class DrawerState2(
|
||||||
|
initialValue: DrawerValue,
|
||||||
|
val confirmStateChange: (DrawerValue) -> Boolean = { true },
|
||||||
|
) {
|
||||||
|
|
||||||
|
val anchoredDraggableState = AnchoredDraggableState(
|
||||||
|
initialValue = initialValue,
|
||||||
|
snapAnimationSpec = AnimationSpec,
|
||||||
|
decayAnimationSpec = exponentialDecay(7.5f),
|
||||||
|
confirmValueChange = confirmStateChange,
|
||||||
|
positionalThreshold = { distance -> distance * DrawerPositionalThreshold },
|
||||||
|
velocityThreshold = { with(requireDensity()) { DrawerVelocityThreshold.toPx() } },
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the drawer is open.
|
||||||
|
*/
|
||||||
|
val isOpen: Boolean
|
||||||
|
get() = currentValue == DrawerValue.Open
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the drawer is closed.
|
||||||
|
*/
|
||||||
|
val isClosed: Boolean
|
||||||
|
get() = currentValue == DrawerValue.Closed
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current value of the state.
|
||||||
|
*
|
||||||
|
* If no swipe or animation is in progress, this corresponds to the start the drawer
|
||||||
|
* currently in. If a swipe or an animation is in progress, this corresponds the state drawer
|
||||||
|
* was in before the swipe or animation started.
|
||||||
|
*/
|
||||||
|
val currentValue: DrawerValue
|
||||||
|
get() {
|
||||||
|
return anchoredDraggableState.currentValue
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the state is currently animating.
|
||||||
|
*/
|
||||||
|
val isAnimationRunning: Boolean
|
||||||
|
get() {
|
||||||
|
return anchoredDraggableState.isAnimationRunning
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open the drawer with animation and suspend until it if fully opened or animation has been
|
||||||
|
* cancelled. This method will throw [CancellationException] if the animation is
|
||||||
|
* interrupted
|
||||||
|
*
|
||||||
|
* @return the reason the open animation ended
|
||||||
|
*/
|
||||||
|
suspend fun open() = animateTo(DrawerValue.Open)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close the drawer with animation and suspend until it if fully closed or animation has been
|
||||||
|
* cancelled. This method will throw [CancellationException] if the animation is
|
||||||
|
* interrupted
|
||||||
|
*
|
||||||
|
* @return the reason the close animation ended
|
||||||
|
*/
|
||||||
|
suspend fun close() = animateTo(DrawerValue.Closed)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the state of the drawer with specific animation
|
||||||
|
*
|
||||||
|
* @param targetValue The new value to animate to.
|
||||||
|
* @param anim The animation that will be used to animate to the new value.
|
||||||
|
*/
|
||||||
|
@Deprecated(
|
||||||
|
message = "This method has been replaced by the open and close methods. The animation " +
|
||||||
|
"spec is now an implementation detail of ModalDrawer.",
|
||||||
|
)
|
||||||
|
suspend fun animateTo(targetValue: DrawerValue, anim: AnimationSpec<Float>) {
|
||||||
|
animateTo(targetValue = targetValue, animationSpec = anim)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the state without any animation and suspend until it's set
|
||||||
|
*
|
||||||
|
* @param targetValue The new target value
|
||||||
|
*/
|
||||||
|
suspend fun snapTo(targetValue: DrawerValue) {
|
||||||
|
anchoredDraggableState.snapTo(targetValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The target value of the drawer state.
|
||||||
|
*
|
||||||
|
* If a swipe is in progress, this is the value that the Drawer 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: DrawerValue
|
||||||
|
get() = anchoredDraggableState.targetValue
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current position (in pixels) of the drawer sheet, or Float.NaN before the offset is
|
||||||
|
* initialized.
|
||||||
|
*
|
||||||
|
* @see [AnchoredDraggableState.offset] for more information.
|
||||||
|
*/
|
||||||
|
@Deprecated(
|
||||||
|
message = "Please access the offset through currentOffset, which returns the value " +
|
||||||
|
"directly instead of wrapping it in a state object.",
|
||||||
|
replaceWith = ReplaceWith("currentOffset"),
|
||||||
|
)
|
||||||
|
val offset: State<Float> = object : State<Float> {
|
||||||
|
override val value: Float get() = anchoredDraggableState.offset
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current position (in pixels) of the drawer sheet, or Float.NaN before the offset is
|
||||||
|
* initialized.
|
||||||
|
*
|
||||||
|
* @see [AnchoredDraggableState.offset] for more information.
|
||||||
|
*/
|
||||||
|
val currentOffset: Float get() = anchoredDraggableState.offset
|
||||||
|
|
||||||
|
internal var density: Density? by mutableStateOf(null)
|
||||||
|
|
||||||
|
private fun requireDensity() = requireNotNull(density) {
|
||||||
|
"The density on BottomDrawerState ($this) was not set. Did you use BottomDrawer" +
|
||||||
|
" with the BottomDrawer composable?"
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun requireOffset(): Float = anchoredDraggableState.requireOffset()
|
||||||
|
|
||||||
|
private suspend fun animateTo(
|
||||||
|
targetValue: DrawerValue,
|
||||||
|
animationSpec: AnimationSpec<Float> = AnimationSpec,
|
||||||
|
velocity: Float = anchoredDraggableState.lastVelocity,
|
||||||
|
) {
|
||||||
|
anchoredDraggableState.anchoredDrag(targetValue = targetValue) { anchors, latestTarget ->
|
||||||
|
val targetOffset = anchors.positionOf(latestTarget)
|
||||||
|
if (!targetOffset.isNaN()) {
|
||||||
|
var prev = if (currentOffset.isNaN()) 0f else currentOffset
|
||||||
|
animate(prev, targetOffset, velocity, animationSpec) { value, velocity ->
|
||||||
|
// Our onDrag coerces the value within the bounds, but an animation may
|
||||||
|
// overshoot, for example a spring animation or an overshooting interpolator
|
||||||
|
// We respect the user's intention and allow the overshoot, but still use
|
||||||
|
// DraggableState's drag for its mutex.
|
||||||
|
dragTo(value, velocity)
|
||||||
|
prev = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* The default [Saver] implementation for [DrawerState2].
|
||||||
|
*/
|
||||||
|
fun Saver(confirmStateChange: (DrawerValue) -> Boolean) =
|
||||||
|
Saver<DrawerState2, DrawerValue>(
|
||||||
|
save = { it.currentValue },
|
||||||
|
restore = { DrawerState2(it, confirmStateChange) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create and [remember] a [DrawerState2].
|
||||||
|
*
|
||||||
|
* @param initialValue The initial value of the state.
|
||||||
|
* @param confirmStateChange Optional callback invoked to confirm or veto a pending state change.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun rememberDrawerState2(
|
||||||
|
initialValue: DrawerValue,
|
||||||
|
confirmStateChange: (DrawerValue) -> Boolean = { true },
|
||||||
|
): DrawerState2 {
|
||||||
|
return rememberSaveable(saver = DrawerState2.Saver(confirmStateChange)) {
|
||||||
|
DrawerState2(initialValue, confirmStateChange)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ModalSideDrawer(
|
||||||
|
drawerContent: @Composable () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
drawerState: DrawerState2 = rememberDrawerState2(DrawerValue.Closed),
|
||||||
|
gesturesEnabled: Boolean = true,
|
||||||
|
scrimColor: Color = DrawerDefaults.scrimColor,
|
||||||
|
content: @Composable () -> Unit,
|
||||||
|
) {
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
val density = LocalDensity.current
|
||||||
|
var maxValue by remember { mutableFloatStateOf(with(density) { DrawerDefaults.MaximumDrawerWidth.toPx() }) }
|
||||||
|
val minValue = 0f
|
||||||
|
|
||||||
|
SideEffect {
|
||||||
|
drawerState.density = density
|
||||||
|
drawerState.anchoredDraggableState.updateAnchors(
|
||||||
|
DraggableAnchors {
|
||||||
|
DrawerValue.Closed at maxValue
|
||||||
|
DrawerValue.Open at minValue
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier.fillMaxSize().anchoredHorizontalDraggable(
|
||||||
|
state = drawerState.anchoredDraggableState,
|
||||||
|
enableDragFromStartToEnd = drawerState.isOpen,
|
||||||
|
enableDragFromEndToStart = drawerState.isClosed,
|
||||||
|
enabled = gesturesEnabled,
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
val radius by remember {
|
||||||
|
snapshotFlow {
|
||||||
|
val step = calculateFraction(maxValue, minValue, 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(maxValue, minValue, 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(placeable.width - scaledWidth, 0)
|
||||||
|
}
|
||||||
|
}.background(
|
||||||
|
color = MaterialTheme.colorScheme.surfaceColorAtElevation(absoluteElevation),
|
||||||
|
shape = ShapeDefaults.Large.copy(topEnd = CornerSize(0), bottomEnd = CornerSize(0)),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Modifier.graphicsLayer {
|
||||||
|
val scale = lerp(1f, 0.95f, predictiveState)
|
||||||
|
scaleX = scale
|
||||||
|
scaleY = scale
|
||||||
|
}.offset {
|
||||||
|
IntOffset(lerp(0f, maxValue * 0.05f, predictiveState).roundToInt(), 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Modifier.onSizeChanged {
|
||||||
|
val width = it.width
|
||||||
|
if (width != 0) {
|
||||||
|
maxValue = width.toFloat()
|
||||||
|
drawerState.anchoredDraggableState.updateAnchors(
|
||||||
|
DraggableAnchors {
|
||||||
|
DrawerValue.Closed at maxValue
|
||||||
|
DrawerValue.Open at minValue
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.offset {
|
||||||
|
IntOffset(drawerState.requireOffset().roundToInt(), 0)
|
||||||
|
}.align(Alignment.CenterEnd) then predictiveModifier,
|
||||||
|
contentAlignment = Alignment.CenterStart,
|
||||||
|
) {
|
||||||
|
drawerContent()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("SameParameterValue")
|
||||||
|
private fun calculateFraction(a: Float, b: Float, pos: Float) = ((pos - a) / (b - a)).coerceIn(0f, 1f)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun Scrim(open: Boolean, onClose: () -> Unit, fraction: () -> Float, color: Color) {
|
||||||
|
val dismissDrawer = if (open) {
|
||||||
|
Modifier.pointerInput(onClose) { detectTapGestures { onClose() } }
|
||||||
|
} else {
|
||||||
|
Modifier
|
||||||
|
}
|
||||||
|
|
||||||
|
Canvas(Modifier.fillMaxSize().then(dismissDrawer)) {
|
||||||
|
drawRect(color, alpha = fraction())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,255 @@
|
|||||||
|
/*
|
||||||
|
* 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.animation.core.exponentialDecay
|
||||||
|
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 androidx.compose.ui.unit.dp
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
|
|
||||||
|
private val SnapAnimationSpec = SpringSpec<Float>()
|
||||||
|
|
||||||
|
// TODO: Update to M3 spec once it's available
|
||||||
|
private val DecayAnimationSpec = exponentialDecay<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.
|
||||||
|
* @param positionalThreshold The positional threshold to be used when calculating the target state
|
||||||
|
* 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 },
|
||||||
|
positionalThreshold: (totalDistance: Float) -> Float,
|
||||||
|
) {
|
||||||
|
internal val anchoredDraggableState =
|
||||||
|
AnchoredDraggableState(
|
||||||
|
initialValue = initialValue,
|
||||||
|
snapAnimationSpec = SnapAnimationSpec,
|
||||||
|
decayAnimationSpec = DecayAnimationSpec,
|
||||||
|
confirmValueChange = confirmValueChange,
|
||||||
|
positionalThreshold = positionalThreshold,
|
||||||
|
velocityThreshold = { with(density) { DismissVelocityThreshold.toPx() } },
|
||||||
|
)
|
||||||
|
|
||||||
|
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,
|
||||||
|
positionalThreshold: (totalDistance: Float) -> Float,
|
||||||
|
density: Density,
|
||||||
|
) = Saver<SwipeToDismissBoxState, SwipeToDismissBoxValue>(
|
||||||
|
save = { it.currentValue },
|
||||||
|
restore = {
|
||||||
|
SwipeToDismissBoxState(
|
||||||
|
it,
|
||||||
|
density,
|
||||||
|
confirmValueChange,
|
||||||
|
positionalThreshold,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
* @param positionalThreshold The positional threshold to be used when calculating the target state
|
||||||
|
* 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 },
|
||||||
|
positionalThreshold: (totalDistance: Float) -> Float =
|
||||||
|
SwipeToDismissBoxDefaults.positionalThreshold,
|
||||||
|
): SwipeToDismissBoxState {
|
||||||
|
val density = LocalDensity.current
|
||||||
|
return rememberSaveable(
|
||||||
|
saver = SwipeToDismissBoxState.Saver(
|
||||||
|
confirmValueChange = confirmValueChange,
|
||||||
|
density = density,
|
||||||
|
positionalThreshold = positionalThreshold,
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
SwipeToDismissBoxState(initialValue, density, confirmValueChange, positionalThreshold)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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,
|
||||||
|
),
|
||||||
|
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
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val DismissVelocityThreshold = 125.dp
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
package androidx.navigation.ui
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import androidx.annotation.IdRes
|
|
||||||
import androidx.navigation.ActivityNavigator
|
|
||||||
import androidx.navigation.NavController
|
|
||||||
import androidx.navigation.NavDestination
|
|
||||||
import androidx.navigation.NavDestination.Companion.hierarchy
|
|
||||||
import androidx.navigation.NavOptions
|
|
||||||
|
|
||||||
@SuppressLint("PrivateResource")
|
|
||||||
fun onNavDestinationSelected2(@IdRes id: Int, navController: NavController): Boolean {
|
|
||||||
if (navController.currentDestination?.matchDestination(id) == true) return true
|
|
||||||
val builder = NavOptions.Builder().setLaunchSingleTop(true)
|
|
||||||
if (navController.currentDestination!!.parent!!.findNode(id) is ActivityNavigator.Destination) {
|
|
||||||
builder.setEnterAnim(R.anim.nav_default_enter_anim)
|
|
||||||
.setExitAnim(R.anim.nav_default_exit_anim)
|
|
||||||
.setPopEnterAnim(R.anim.nav_default_pop_enter_anim)
|
|
||||||
.setPopExitAnim(R.anim.nav_default_pop_exit_anim)
|
|
||||||
} else {
|
|
||||||
builder.setEnterAnim(androidx.fragment.R.animator.fragment_open_enter)
|
|
||||||
.setExitAnim(androidx.fragment.R.animator.fragment_open_exit)
|
|
||||||
.setPopEnterAnim(androidx.fragment.R.animator.fragment_close_enter)
|
|
||||||
.setPopExitAnim(androidx.fragment.R.animator.fragment_close_exit)
|
|
||||||
}
|
|
||||||
val options = builder.build()
|
|
||||||
return try {
|
|
||||||
// TODO provide proper API instead of using Exceptions as Control-Flow.
|
|
||||||
navController.navigate(id, null, options)
|
|
||||||
// Return true only if the destination we've navigated to matches the MenuItem
|
|
||||||
navController.currentDestination?.matchDestination(id) == true
|
|
||||||
} catch (e: IllegalArgumentException) {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun NavDestination.matchDestination(@IdRes destId: Int): Boolean =
|
|
||||||
hierarchy.any { it.id == destId }
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
package androidx.swiperefreshlayout.widget
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.Context
|
|
||||||
import android.graphics.drawable.ShapeDrawable
|
|
||||||
import android.util.AttributeSet
|
|
||||||
import androidx.core.graphics.ColorUtils
|
|
||||||
import rikka.core.res.resolveColor
|
|
||||||
|
|
||||||
@SuppressLint("PrivateResource")
|
|
||||||
class ThemedSwipeRefreshLayout @JvmOverloads constructor(
|
|
||||||
context: Context,
|
|
||||||
attrs: AttributeSet? = null,
|
|
||||||
) : SwipeRefreshLayout(context, attrs) {
|
|
||||||
|
|
||||||
init {
|
|
||||||
val surfaceColor =
|
|
||||||
context.theme.resolveColor(com.google.android.material.R.attr.colorSurface)
|
|
||||||
val overlayColor =
|
|
||||||
context.getColor(com.google.android.material.R.color.m3_popupmenu_overlay_color)
|
|
||||||
val backgroundColor = ColorUtils.compositeColors(overlayColor, surfaceColor)
|
|
||||||
(mCircleView.background as ShapeDrawable).paint.color = backgroundColor
|
|
||||||
setColorSchemeColors(context.theme.resolveColor(com.google.android.material.R.attr.colorAccent))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
package com.davemorrissey.labs.subscaleview
|
|
||||||
|
|
||||||
import android.graphics.Bitmap
|
|
||||||
import android.graphics.Rect
|
|
||||||
import com.davemorrissey.labs.subscaleview.provider.InputProvider
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper class used to set the source and additional attributes from a variety of sources. Supports
|
|
||||||
* use of a bitmap, asset, resource, external file or any other URI.
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* When you are using a preview image, you must set the dimensions of the full size image on the
|
|
||||||
* ImageSource object for the full size image using the [.dimensions] method.
|
|
||||||
*/
|
|
||||||
class ImageSource {
|
|
||||||
val bitmap: Bitmap?
|
|
||||||
val provider: InputProvider?
|
|
||||||
|
|
||||||
private var sWidth = 0
|
|
||||||
private var sHeight = 0
|
|
||||||
|
|
||||||
var sRegion = Rect(0, 0, 0, 0)
|
|
||||||
private set
|
|
||||||
var isCached = false
|
|
||||||
private set
|
|
||||||
|
|
||||||
private constructor(bitmap: Bitmap, cached: Boolean) {
|
|
||||||
this.bitmap = bitmap
|
|
||||||
provider = null
|
|
||||||
sWidth = bitmap.width
|
|
||||||
sHeight = bitmap.height
|
|
||||||
isCached = cached
|
|
||||||
}
|
|
||||||
|
|
||||||
private constructor(provider: InputProvider) {
|
|
||||||
bitmap = null
|
|
||||||
this.provider = provider
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Use a region of the source image. Region must be set independently for the full size image and the preview if
|
|
||||||
* you are using one.
|
|
||||||
*
|
|
||||||
* @param sRegion the region of the source image to be displayed.
|
|
||||||
* @return this instance for chaining.
|
|
||||||
*/
|
|
||||||
fun region(sRegion: Rect): ImageSource {
|
|
||||||
this.sRegion = sRegion
|
|
||||||
setInvariants()
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Declare the dimensions of the image. This is only required for a full size image, when you are specifying a URI
|
|
||||||
* and also a preview image. When displaying a bitmap object, or not using a preview, you do not need to declare
|
|
||||||
* the image dimensions. Note if the declared dimensions are found to be incorrect, the view will reset.
|
|
||||||
*
|
|
||||||
* @param sWidth width of the source image.
|
|
||||||
* @param sHeight height of the source image.
|
|
||||||
* @return this instance for chaining.
|
|
||||||
*/
|
|
||||||
fun dimensions(sWidth: Int, sHeight: Int): ImageSource {
|
|
||||||
if (bitmap == null) {
|
|
||||||
this.sWidth = sWidth
|
|
||||||
this.sHeight = sHeight
|
|
||||||
}
|
|
||||||
setInvariants()
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setInvariants() {
|
|
||||||
if (!sRegion.isEmpty) {
|
|
||||||
sWidth = sRegion.width()
|
|
||||||
sHeight = sRegion.height()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
/**
|
|
||||||
* Create an instance from an input provider.
|
|
||||||
*
|
|
||||||
* @param provider input stream provider.
|
|
||||||
* @return an [ImageSource] instance.
|
|
||||||
*/
|
|
||||||
fun provider(provider: InputProvider): ImageSource {
|
|
||||||
return ImageSource(provider)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provide a loaded bitmap for display.
|
|
||||||
*
|
|
||||||
* @param bitmap bitmap to be displayed.
|
|
||||||
* @return an [ImageSource] instance.
|
|
||||||
*/
|
|
||||||
fun bitmap(bitmap: Bitmap): ImageSource {
|
|
||||||
return ImageSource(bitmap, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provide a loaded and cached bitmap for display. This bitmap will not be recycled when it is no
|
|
||||||
* longer needed. Use this method if you loaded the bitmap with an image loader such as Picasso
|
|
||||||
* or Volley.
|
|
||||||
*
|
|
||||||
* @param bitmap bitmap to be displayed.
|
|
||||||
* @return an [ImageSource] instance.
|
|
||||||
*/
|
|
||||||
fun cachedBitmap(bitmap: Bitmap): ImageSource {
|
|
||||||
return ImageSource(bitmap, true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
package com.davemorrissey.labs.subscaleview
|
|
||||||
|
|
||||||
import android.graphics.PointF
|
|
||||||
import java.io.Serializable
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wraps the scale, center and orientation of a displayed image for easy restoration on screen rotate.
|
|
||||||
*/
|
|
||||||
class ImageViewState(val scale: Float, center: PointF) : Serializable {
|
|
||||||
private val centerX: Float = center.x
|
|
||||||
private val centerY: Float = center.y
|
|
||||||
|
|
||||||
val center: PointF
|
|
||||||
get() = PointF(centerX, centerY)
|
|
||||||
}
|
|
||||||
+47
-1158
File diff suppressed because it is too large.
Load diff
@@ -1,50 +0,0 @@
|
|||||||
package com.davemorrissey.labs.subscaleview.decoder
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.graphics.Bitmap
|
|
||||||
import android.graphics.Point
|
|
||||||
import android.graphics.Rect
|
|
||||||
import com.davemorrissey.labs.subscaleview.provider.InputProvider
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Interface for image decoding classes.
|
|
||||||
*/
|
|
||||||
interface Decoder {
|
|
||||||
/**
|
|
||||||
* Initialise the decoder. When possible, perform initial setup work once in this method. The
|
|
||||||
* dimensions of the image must be returned.
|
|
||||||
*
|
|
||||||
* @param context Application context. A reference may be held, but must be cleared on recycle.
|
|
||||||
* @param provider Provider of the image.
|
|
||||||
* @return Dimensions of the image.
|
|
||||||
* @throws Exception if initialisation fails.
|
|
||||||
*/
|
|
||||||
fun init(context: Context, provider: InputProvider): Point
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decode a region of the image with the given sample size. This method is called off the UI
|
|
||||||
* thread so it can safely load the image on the current thread. It is called from
|
|
||||||
* [android.os.AsyncTask]s running in an executor that may have multiple threads, so
|
|
||||||
* implementations must be thread safe. Adding `synchronized` to the method signature
|
|
||||||
* is the simplest way to achieve this, but bear in mind the [.recycle] method can be
|
|
||||||
* called concurrently.
|
|
||||||
*
|
|
||||||
* @param sRect Source image rectangle to decode.
|
|
||||||
* @param sampleSize Sample size.
|
|
||||||
* @return The decoded region. It is safe to return null if decoding fails.
|
|
||||||
*/
|
|
||||||
fun decodeRegion(sRect: Rect, sampleSize: Int): Bitmap
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Status check. Should return false before initialisation and after recycle.
|
|
||||||
*
|
|
||||||
* @return true if the decoder is ready to be used.
|
|
||||||
*/
|
|
||||||
val isReady: Boolean
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This method will be called when the decoder is no longer required. It should clean up any
|
|
||||||
* resources still in use.
|
|
||||||
*/
|
|
||||||
fun recycle()
|
|
||||||
}
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
package com.davemorrissey.labs.subscaleview.decoder
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.graphics.Bitmap
|
|
||||||
import android.graphics.Point
|
|
||||||
import android.graphics.Rect
|
|
||||||
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
|
||||||
import com.davemorrissey.labs.subscaleview.provider.InputProvider
|
|
||||||
|
|
||||||
class ImageDecoder(private val cropBorders: Boolean, bitmapConfig: Bitmap.Config? = null) : Decoder {
|
|
||||||
init {
|
|
||||||
val globalBitmapConfig = SubsamplingScaleImageView.preferredBitmapConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialise the decoder. When possible, perform initial setup work once in this method. The
|
|
||||||
* dimensions of the image must be returned.
|
|
||||||
*
|
|
||||||
* @param context Application context. A reference may be held, but must be cleared on recycle.
|
|
||||||
* @param provider Provider of the image.
|
|
||||||
* @return Dimensions of the image.
|
|
||||||
* @throws Exception if initialisation fails.
|
|
||||||
*/
|
|
||||||
override fun init(context: Context, provider: InputProvider): Point {
|
|
||||||
TODO()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decode a region of the image with the given sample size. This method is called off the UI
|
|
||||||
* thread so it can safely load the image on the current thread. It is called from
|
|
||||||
* [android.os.AsyncTask]s running in an executor that may have multiple threads, so
|
|
||||||
* implementations must be thread safe. Adding `synchronized` to the method signature
|
|
||||||
* is the simplest way to achieve this, but bear in mind the [.recycle] method can be
|
|
||||||
* called concurrently.
|
|
||||||
*
|
|
||||||
* @param sRect Source image rectangle to decode.
|
|
||||||
* @param sampleSize Sample size.
|
|
||||||
* @return The decoded region. It is safe to return null if decoding fails.
|
|
||||||
*/
|
|
||||||
override fun decodeRegion(sRect: Rect, sampleSize: Int): Bitmap {
|
|
||||||
TODO()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Status check. Should return false before initialisation and after recycle.
|
|
||||||
*
|
|
||||||
* @return true if the decoder is ready to be used.
|
|
||||||
*/
|
|
||||||
override val isReady: Boolean = TODO()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This method will be called when the decoder is no longer required. It should clean up any
|
|
||||||
* resources still in use.
|
|
||||||
*/
|
|
||||||
override fun recycle() {
|
|
||||||
TODO()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
package com.davemorrissey.labs.subscaleview.provider
|
|
||||||
|
|
||||||
interface InputProvider
|
|
||||||
@@ -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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,653 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
*
|
||||||
|
* 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.web
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.ViewGroup.LayoutParams
|
||||||
|
import android.webkit.WebChromeClient
|
||||||
|
import android.webkit.WebResourceError
|
||||||
|
import android.webkit.WebResourceRequest
|
||||||
|
import android.webkit.WebView
|
||||||
|
import android.webkit.WebViewClient
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import androidx.activity.compose.BackHandler
|
||||||
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.Stable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateListOf
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.runtime.snapshotFlow
|
||||||
|
import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
|
import com.google.accompanist.web.LoadingState.Finished
|
||||||
|
import com.google.accompanist.web.LoadingState.Loading
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A wrapper around the Android View WebView to provide a basic WebView composable.
|
||||||
|
*
|
||||||
|
* If you require more customisation you are most likely better rolling your own and using this
|
||||||
|
* wrapper as an example.
|
||||||
|
*
|
||||||
|
* The WebView attempts to set the layoutParams based on the Compose modifier passed in. If it
|
||||||
|
* is incorrectly sizing, use the layoutParams composable function instead.
|
||||||
|
*
|
||||||
|
* @param state The webview state holder where the Uri to load is defined.
|
||||||
|
* @param modifier A compose modifier
|
||||||
|
* @param captureBackPresses Set to true to have this Composable capture back presses and navigate
|
||||||
|
* the WebView back.
|
||||||
|
* @param navigator An optional navigator object that can be used to control the WebView's
|
||||||
|
* navigation from outside the composable.
|
||||||
|
* @param onCreated Called when the WebView is first created, this can be used to set additional
|
||||||
|
* settings on the WebView. WebChromeClient and WebViewClient should not be set here as they will be
|
||||||
|
* subsequently overwritten after this lambda is called.
|
||||||
|
* @param onDispose Called when the WebView is destroyed. Provides a bundle which can be saved
|
||||||
|
* if you need to save and restore state in this WebView.
|
||||||
|
* @param client Provides access to WebViewClient via subclassing
|
||||||
|
* @param chromeClient Provides access to WebChromeClient via subclassing
|
||||||
|
* @param factory An optional WebView factory for using a custom subclass of WebView
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun WebView(
|
||||||
|
state: WebViewState,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
captureBackPresses: Boolean = true,
|
||||||
|
navigator: WebViewNavigator = rememberWebViewNavigator(),
|
||||||
|
onCreated: (WebView) -> Unit = {},
|
||||||
|
onDispose: (WebView) -> Unit = {},
|
||||||
|
client: AccompanistWebViewClient = remember { AccompanistWebViewClient() },
|
||||||
|
chromeClient: AccompanistWebChromeClient = remember { AccompanistWebChromeClient() },
|
||||||
|
factory: ((Context) -> WebView)? = null,
|
||||||
|
) {
|
||||||
|
BoxWithConstraints(modifier) {
|
||||||
|
// WebView changes it's layout strategy based on
|
||||||
|
// it's layoutParams. We convert from Compose Modifier to
|
||||||
|
// layout params here.
|
||||||
|
val width =
|
||||||
|
if (constraints.hasFixedWidth) {
|
||||||
|
LayoutParams.MATCH_PARENT
|
||||||
|
} else {
|
||||||
|
LayoutParams.WRAP_CONTENT
|
||||||
|
}
|
||||||
|
val height =
|
||||||
|
if (constraints.hasFixedHeight) {
|
||||||
|
LayoutParams.MATCH_PARENT
|
||||||
|
} else {
|
||||||
|
LayoutParams.WRAP_CONTENT
|
||||||
|
}
|
||||||
|
|
||||||
|
val layoutParams = FrameLayout.LayoutParams(
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
)
|
||||||
|
|
||||||
|
WebView(
|
||||||
|
state,
|
||||||
|
layoutParams,
|
||||||
|
Modifier,
|
||||||
|
captureBackPresses,
|
||||||
|
navigator,
|
||||||
|
onCreated,
|
||||||
|
onDispose,
|
||||||
|
client,
|
||||||
|
chromeClient,
|
||||||
|
factory,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A wrapper around the Android View WebView to provide a basic WebView composable.
|
||||||
|
*
|
||||||
|
* If you require more customisation you are most likely better rolling your own and using this
|
||||||
|
* wrapper as an example.
|
||||||
|
*
|
||||||
|
* The WebView attempts to set the layoutParams based on the Compose modifier passed in. If it
|
||||||
|
* is incorrectly sizing, use the layoutParams composable function instead.
|
||||||
|
*
|
||||||
|
* @param state The webview state holder where the Uri to load is defined.
|
||||||
|
* @param layoutParams A FrameLayout.LayoutParams object to custom size the underlying WebView.
|
||||||
|
* @param modifier A compose modifier
|
||||||
|
* @param captureBackPresses Set to true to have this Composable capture back presses and navigate
|
||||||
|
* the WebView back.
|
||||||
|
* @param navigator An optional navigator object that can be used to control the WebView's
|
||||||
|
* navigation from outside the composable.
|
||||||
|
* @param onCreated Called when the WebView is first created, this can be used to set additional
|
||||||
|
* settings on the WebView. WebChromeClient and WebViewClient should not be set here as they will be
|
||||||
|
* subsequently overwritten after this lambda is called.
|
||||||
|
* @param onDispose Called when the WebView is destroyed. Provides a bundle which can be saved
|
||||||
|
* if you need to save and restore state in this WebView.
|
||||||
|
* @param client Provides access to WebViewClient via subclassing
|
||||||
|
* @param chromeClient Provides access to WebChromeClient via subclassing
|
||||||
|
* @param factory An optional WebView factory for using a custom subclass of WebView
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun WebView(
|
||||||
|
state: WebViewState,
|
||||||
|
layoutParams: FrameLayout.LayoutParams,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
captureBackPresses: Boolean = true,
|
||||||
|
navigator: WebViewNavigator = rememberWebViewNavigator(),
|
||||||
|
onCreated: (WebView) -> Unit = {},
|
||||||
|
onDispose: (WebView) -> Unit = {},
|
||||||
|
client: AccompanistWebViewClient = remember { AccompanistWebViewClient() },
|
||||||
|
chromeClient: AccompanistWebChromeClient = remember { AccompanistWebChromeClient() },
|
||||||
|
factory: ((Context) -> WebView)? = null,
|
||||||
|
) {
|
||||||
|
val webView = state.webView
|
||||||
|
|
||||||
|
BackHandler(captureBackPresses && navigator.canGoBack) {
|
||||||
|
webView?.goBack()
|
||||||
|
}
|
||||||
|
|
||||||
|
webView?.let { wv ->
|
||||||
|
LaunchedEffect(wv, navigator) {
|
||||||
|
with(navigator) {
|
||||||
|
wv.handleNavigationEvents()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(wv, state) {
|
||||||
|
snapshotFlow { state.content }.collect { content ->
|
||||||
|
when (content) {
|
||||||
|
is WebContent.Url -> {
|
||||||
|
wv.loadUrl(content.url, content.additionalHttpHeaders)
|
||||||
|
}
|
||||||
|
|
||||||
|
is WebContent.Data -> {
|
||||||
|
wv.loadDataWithBaseURL(
|
||||||
|
content.baseUrl,
|
||||||
|
content.data,
|
||||||
|
content.mimeType,
|
||||||
|
content.encoding,
|
||||||
|
content.historyUrl,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is WebContent.Post -> {
|
||||||
|
wv.postUrl(
|
||||||
|
content.url,
|
||||||
|
content.postData,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is WebContent.NavigatorOnly -> {
|
||||||
|
// NO-OP
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the state of the client and chrome client
|
||||||
|
// This is done internally to ensure they always are the same instance as the
|
||||||
|
// parent Web composable
|
||||||
|
client.state = state
|
||||||
|
client.navigator = navigator
|
||||||
|
chromeClient.state = state
|
||||||
|
|
||||||
|
AndroidView(
|
||||||
|
factory = { context ->
|
||||||
|
(factory?.invoke(context) ?: WebView(context)).apply {
|
||||||
|
onCreated(this)
|
||||||
|
|
||||||
|
this.layoutParams = layoutParams
|
||||||
|
|
||||||
|
state.viewState?.let {
|
||||||
|
this.restoreState(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
webChromeClient = chromeClient
|
||||||
|
webViewClient = client
|
||||||
|
}.also { state.webView = it }
|
||||||
|
},
|
||||||
|
modifier = modifier,
|
||||||
|
onRelease = {
|
||||||
|
onDispose(it)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AccompanistWebViewClient
|
||||||
|
*
|
||||||
|
* A parent class implementation of WebViewClient that can be subclassed to add custom behaviour.
|
||||||
|
*
|
||||||
|
* As Accompanist Web needs to set its own web client to function, it provides this intermediary
|
||||||
|
* class that can be overriden if further custom behaviour is required.
|
||||||
|
*/
|
||||||
|
open class AccompanistWebViewClient : WebViewClient() {
|
||||||
|
open lateinit var state: WebViewState
|
||||||
|
internal set
|
||||||
|
open lateinit var navigator: WebViewNavigator
|
||||||
|
internal set
|
||||||
|
|
||||||
|
override fun onPageStarted(view: WebView, url: String?, favicon: Bitmap?) {
|
||||||
|
super.onPageStarted(view, url, favicon)
|
||||||
|
state.loadingState = Loading(0.0f)
|
||||||
|
state.errorsForCurrentRequest.clear()
|
||||||
|
state.pageTitle = null
|
||||||
|
state.pageIcon = null
|
||||||
|
|
||||||
|
state.lastLoadedUrl = url
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPageFinished(view: WebView, url: String?) {
|
||||||
|
super.onPageFinished(view, url)
|
||||||
|
state.loadingState = Finished
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun doUpdateVisitedHistory(view: WebView, url: String?, isReload: Boolean) {
|
||||||
|
super.doUpdateVisitedHistory(view, url, isReload)
|
||||||
|
|
||||||
|
navigator.canGoBack = view.canGoBack()
|
||||||
|
navigator.canGoForward = view.canGoForward()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onReceivedError(
|
||||||
|
view: WebView,
|
||||||
|
request: WebResourceRequest?,
|
||||||
|
error: WebResourceError?,
|
||||||
|
) {
|
||||||
|
super.onReceivedError(view, request, error)
|
||||||
|
|
||||||
|
if (error != null) {
|
||||||
|
state.errorsForCurrentRequest.add(WebViewError(request, error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AccompanistWebChromeClient
|
||||||
|
*
|
||||||
|
* A parent class implementation of WebChromeClient that can be subclassed to add custom behaviour.
|
||||||
|
*
|
||||||
|
* As Accompanist Web needs to set its own web client to function, it provides this intermediary
|
||||||
|
* class that can be overriden if further custom behaviour is required.
|
||||||
|
*/
|
||||||
|
open class AccompanistWebChromeClient : WebChromeClient() {
|
||||||
|
open lateinit var state: WebViewState
|
||||||
|
internal set
|
||||||
|
|
||||||
|
override fun onReceivedTitle(view: WebView, title: String?) {
|
||||||
|
super.onReceivedTitle(view, title)
|
||||||
|
state.pageTitle = title
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onReceivedIcon(view: WebView, icon: Bitmap?) {
|
||||||
|
super.onReceivedIcon(view, icon)
|
||||||
|
state.pageIcon = icon
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onProgressChanged(view: WebView, newProgress: Int) {
|
||||||
|
super.onProgressChanged(view, newProgress)
|
||||||
|
if (state.loadingState is Finished) return
|
||||||
|
state.loadingState = Loading(newProgress / 100.0f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class WebContent {
|
||||||
|
data class Url(
|
||||||
|
val url: String,
|
||||||
|
val additionalHttpHeaders: Map<String, String> = emptyMap(),
|
||||||
|
) : WebContent()
|
||||||
|
|
||||||
|
data class Data(
|
||||||
|
val data: String,
|
||||||
|
val baseUrl: String? = null,
|
||||||
|
val encoding: String = "utf-8",
|
||||||
|
val mimeType: String? = null,
|
||||||
|
val historyUrl: String? = null,
|
||||||
|
) : WebContent()
|
||||||
|
|
||||||
|
data class Post(
|
||||||
|
val url: String,
|
||||||
|
val postData: ByteArray,
|
||||||
|
) : WebContent() {
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (javaClass != other?.javaClass) return false
|
||||||
|
|
||||||
|
other as Post
|
||||||
|
|
||||||
|
if (url != other.url) return false
|
||||||
|
if (!postData.contentEquals(other.postData)) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = url.hashCode()
|
||||||
|
result = 31 * result + postData.contentHashCode()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data object NavigatorOnly : WebContent()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sealed class for constraining possible loading states.
|
||||||
|
* See [Loading] and [Finished].
|
||||||
|
*/
|
||||||
|
sealed class LoadingState {
|
||||||
|
/**
|
||||||
|
* Describes a WebView that has not yet loaded for the first time.
|
||||||
|
*/
|
||||||
|
data object Initializing : LoadingState()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Describes a webview between `onPageStarted` and `onPageFinished` events, contains a
|
||||||
|
* [progress] property which is updated by the webview.
|
||||||
|
*/
|
||||||
|
data class Loading(val progress: Float) : LoadingState()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Describes a webview that has finished loading content.
|
||||||
|
*/
|
||||||
|
data object Finished : LoadingState()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A state holder to hold the state for the WebView. In most cases this will be remembered
|
||||||
|
* using the rememberWebViewState(uri) function.
|
||||||
|
*/
|
||||||
|
@Stable
|
||||||
|
class WebViewState(webContent: WebContent) {
|
||||||
|
var lastLoadedUrl: String? by mutableStateOf(null)
|
||||||
|
internal set
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The content being loaded by the WebView
|
||||||
|
*/
|
||||||
|
var content: WebContent by mutableStateOf(webContent)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the WebView is currently [LoadingState.Loading] data in its main frame (along with
|
||||||
|
* progress) or the data loading has [LoadingState.Finished]. See [LoadingState]
|
||||||
|
*/
|
||||||
|
var loadingState: LoadingState by mutableStateOf(LoadingState.Initializing)
|
||||||
|
internal set
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the webview is currently loading data in its main frame
|
||||||
|
*/
|
||||||
|
val isLoading: Boolean
|
||||||
|
get() = loadingState !is Finished
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The title received from the loaded content of the current page
|
||||||
|
*/
|
||||||
|
var pageTitle: String? by mutableStateOf(null)
|
||||||
|
internal set
|
||||||
|
|
||||||
|
/**
|
||||||
|
* the favicon received from the loaded content of the current page
|
||||||
|
*/
|
||||||
|
var pageIcon: Bitmap? by mutableStateOf(null)
|
||||||
|
internal set
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A list for errors captured in the last load. Reset when a new page is loaded.
|
||||||
|
* Errors could be from any resource (iframe, image, etc.), not just for the main page.
|
||||||
|
* For more fine grained control use the OnError callback of the WebView.
|
||||||
|
*/
|
||||||
|
val errorsForCurrentRequest: SnapshotStateList<WebViewError> = mutableStateListOf()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The saved view state from when the view was destroyed last. To restore state,
|
||||||
|
* use the navigator and only call loadUrl if the bundle is null.
|
||||||
|
* See WebViewSaveStateSample.
|
||||||
|
*/
|
||||||
|
var viewState: Bundle? = null
|
||||||
|
internal set
|
||||||
|
|
||||||
|
// We need access to this in the state saver. An internal DisposableEffect or AndroidView
|
||||||
|
// onDestroy is called after the state saver and so can't be used.
|
||||||
|
internal var webView by mutableStateOf<WebView?>(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows control over the navigation of a WebView from outside the composable. E.g. for performing
|
||||||
|
* a back navigation in response to the user clicking the "up" button in a TopAppBar.
|
||||||
|
*
|
||||||
|
* @see [rememberWebViewNavigator]
|
||||||
|
*/
|
||||||
|
@Stable
|
||||||
|
class WebViewNavigator(private val coroutineScope: CoroutineScope) {
|
||||||
|
private sealed interface NavigationEvent {
|
||||||
|
data object Back : NavigationEvent
|
||||||
|
data object Forward : NavigationEvent
|
||||||
|
data object Reload : NavigationEvent
|
||||||
|
data object StopLoading : NavigationEvent
|
||||||
|
|
||||||
|
data class LoadUrl(
|
||||||
|
val url: String,
|
||||||
|
val additionalHttpHeaders: Map<String, String> = emptyMap(),
|
||||||
|
) : NavigationEvent
|
||||||
|
|
||||||
|
data class LoadHtml(
|
||||||
|
val html: String,
|
||||||
|
val baseUrl: String? = null,
|
||||||
|
val mimeType: String? = null,
|
||||||
|
val encoding: String? = "utf-8",
|
||||||
|
val historyUrl: String? = null,
|
||||||
|
) : NavigationEvent
|
||||||
|
|
||||||
|
data class PostUrl(
|
||||||
|
val url: String,
|
||||||
|
val postData: ByteArray,
|
||||||
|
) : NavigationEvent {
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (javaClass != other?.javaClass) return false
|
||||||
|
|
||||||
|
other as PostUrl
|
||||||
|
|
||||||
|
if (url != other.url) return false
|
||||||
|
if (!postData.contentEquals(other.postData)) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = url.hashCode()
|
||||||
|
result = 31 * result + postData.contentHashCode()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val navigationEvents: MutableSharedFlow<NavigationEvent> = MutableSharedFlow(replay = 1)
|
||||||
|
|
||||||
|
// Use Dispatchers.Main to ensure that the webview methods are called on UI thread
|
||||||
|
internal suspend fun WebView.handleNavigationEvents(): Nothing = withContext(Dispatchers.Main) {
|
||||||
|
navigationEvents.collect { event ->
|
||||||
|
when (event) {
|
||||||
|
is NavigationEvent.Back -> goBack()
|
||||||
|
is NavigationEvent.Forward -> goForward()
|
||||||
|
is NavigationEvent.Reload -> reload()
|
||||||
|
is NavigationEvent.StopLoading -> stopLoading()
|
||||||
|
is NavigationEvent.LoadHtml -> loadDataWithBaseURL(
|
||||||
|
event.baseUrl,
|
||||||
|
event.html,
|
||||||
|
event.mimeType,
|
||||||
|
event.encoding,
|
||||||
|
event.historyUrl,
|
||||||
|
)
|
||||||
|
|
||||||
|
is NavigationEvent.LoadUrl -> {
|
||||||
|
loadUrl(event.url, event.additionalHttpHeaders)
|
||||||
|
}
|
||||||
|
|
||||||
|
is NavigationEvent.PostUrl -> {
|
||||||
|
postUrl(event.url, event.postData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True when the web view is able to navigate backwards, false otherwise.
|
||||||
|
*/
|
||||||
|
var canGoBack: Boolean by mutableStateOf(false)
|
||||||
|
internal set
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True when the web view is able to navigate forwards, false otherwise.
|
||||||
|
*/
|
||||||
|
var canGoForward: Boolean by mutableStateOf(false)
|
||||||
|
internal set
|
||||||
|
|
||||||
|
fun loadUrl(url: String, additionalHttpHeaders: Map<String, String> = emptyMap()) {
|
||||||
|
coroutineScope.launch {
|
||||||
|
navigationEvents.emit(
|
||||||
|
NavigationEvent.LoadUrl(
|
||||||
|
url,
|
||||||
|
additionalHttpHeaders,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadHtml(
|
||||||
|
html: String,
|
||||||
|
baseUrl: String? = null,
|
||||||
|
mimeType: String? = null,
|
||||||
|
encoding: String? = "utf-8",
|
||||||
|
historyUrl: String? = null,
|
||||||
|
) {
|
||||||
|
coroutineScope.launch {
|
||||||
|
navigationEvents.emit(
|
||||||
|
NavigationEvent.LoadHtml(
|
||||||
|
html,
|
||||||
|
baseUrl,
|
||||||
|
mimeType,
|
||||||
|
encoding,
|
||||||
|
historyUrl,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun postUrl(
|
||||||
|
url: String,
|
||||||
|
postData: ByteArray,
|
||||||
|
) {
|
||||||
|
coroutineScope.launch {
|
||||||
|
navigationEvents.emit(
|
||||||
|
NavigationEvent.PostUrl(
|
||||||
|
url,
|
||||||
|
postData,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigates the webview back to the previous page.
|
||||||
|
*/
|
||||||
|
fun navigateBack() {
|
||||||
|
coroutineScope.launch { navigationEvents.emit(NavigationEvent.Back) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigates the webview forward after going back from a page.
|
||||||
|
*/
|
||||||
|
fun navigateForward() {
|
||||||
|
coroutineScope.launch { navigationEvents.emit(NavigationEvent.Forward) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reloads the current page in the webview.
|
||||||
|
*/
|
||||||
|
fun reload() {
|
||||||
|
coroutineScope.launch { navigationEvents.emit(NavigationEvent.Reload) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops the current page load (if one is loading).
|
||||||
|
*/
|
||||||
|
fun stopLoading() {
|
||||||
|
coroutineScope.launch { navigationEvents.emit(NavigationEvent.StopLoading) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates and remembers a [WebViewNavigator] using the default [CoroutineScope] or a provided
|
||||||
|
* override.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun rememberWebViewNavigator(
|
||||||
|
coroutineScope: CoroutineScope = rememberCoroutineScope(),
|
||||||
|
): WebViewNavigator = remember(coroutineScope) { WebViewNavigator(coroutineScope) }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A wrapper class to hold errors from the WebView.
|
||||||
|
*/
|
||||||
|
@Immutable
|
||||||
|
data class WebViewError(
|
||||||
|
/**
|
||||||
|
* The request the error came from.
|
||||||
|
*/
|
||||||
|
val request: WebResourceRequest?,
|
||||||
|
/**
|
||||||
|
* The error that was reported.
|
||||||
|
*/
|
||||||
|
val error: WebResourceError,
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a WebView state that is remembered across Compositions.
|
||||||
|
*
|
||||||
|
* @param url The url to load in the WebView
|
||||||
|
* @param additionalHttpHeaders Optional, additional HTTP headers that are passed to [WebView.loadUrl].
|
||||||
|
* Note that these headers are used for all subsequent requests of the WebView.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun rememberWebViewState(
|
||||||
|
url: String,
|
||||||
|
additionalHttpHeaders: Map<String, String> = emptyMap(),
|
||||||
|
): WebViewState =
|
||||||
|
// Rather than using .apply {} here we will recreate the state, this prevents
|
||||||
|
// a recomposition loop when the webview updates the url itself.
|
||||||
|
remember {
|
||||||
|
WebViewState(
|
||||||
|
WebContent.Url(
|
||||||
|
url = url,
|
||||||
|
additionalHttpHeaders = additionalHttpHeaders,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}.apply {
|
||||||
|
this.content = WebContent.Url(
|
||||||
|
url = url,
|
||||||
|
additionalHttpHeaders = additionalHttpHeaders,
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -17,46 +17,59 @@
|
|||||||
package com.hippo.ehviewer
|
package com.hippo.ehviewer
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.ComponentCallbacks2
|
import android.content.Context
|
||||||
|
import android.os.StrictMode
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
|
import androidx.appcompat.content.res.AppCompatResources
|
||||||
import androidx.collection.LruCache
|
import androidx.collection.LruCache
|
||||||
|
import androidx.compose.runtime.snapshots.Snapshot
|
||||||
import androidx.lifecycle.ProcessLifecycleOwner
|
import androidx.lifecycle.ProcessLifecycleOwner
|
||||||
import androidx.lifecycle.coroutineScope
|
import androidx.lifecycle.coroutineScope
|
||||||
import coil.ImageLoaderFactory
|
import coil3.EventListener
|
||||||
import coil.decode.ImageDecoderDecoder
|
import coil3.SingletonImageLoader
|
||||||
import coil.util.DebugLogger
|
import coil3.asCoilImage
|
||||||
|
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.EhCookieStore
|
||||||
import com.hippo.ehviewer.client.EhTagDatabase
|
import com.hippo.ehviewer.client.EhTagDatabase
|
||||||
import com.hippo.ehviewer.client.data.GalleryDetail
|
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.MergeInterceptor
|
||||||
import com.hippo.ehviewer.coil.installCronetHttpUriFetcher
|
|
||||||
import com.hippo.ehviewer.dailycheck.checkDawn
|
import com.hippo.ehviewer.dailycheck.checkDawn
|
||||||
import com.hippo.ehviewer.dao.SearchDatabase
|
import com.hippo.ehviewer.dao.SearchDatabase
|
||||||
import com.hippo.ehviewer.download.DownloadManager
|
import com.hippo.ehviewer.download.DownloadManager
|
||||||
import com.hippo.ehviewer.ktbuilder.cache
|
import com.hippo.ehviewer.download.DownloadsFilterMode
|
||||||
import com.hippo.ehviewer.ktbuilder.chunker
|
|
||||||
import com.hippo.ehviewer.ktbuilder.diskCache
|
import com.hippo.ehviewer.ktbuilder.diskCache
|
||||||
import com.hippo.ehviewer.ktbuilder.httpClient
|
|
||||||
import com.hippo.ehviewer.ktbuilder.imageLoader
|
import com.hippo.ehviewer.ktbuilder.imageLoader
|
||||||
|
import com.hippo.ehviewer.ktor.CronetEngine
|
||||||
import com.hippo.ehviewer.legacy.cleanObsoleteCache
|
import com.hippo.ehviewer.legacy.cleanObsoleteCache
|
||||||
import com.hippo.ehviewer.legacy.migrateCookies
|
|
||||||
import com.hippo.ehviewer.ui.keepNoMediaFileStatus
|
import com.hippo.ehviewer.ui.keepNoMediaFileStatus
|
||||||
import com.hippo.ehviewer.ui.lockObserver
|
import com.hippo.ehviewer.ui.lockObserver
|
||||||
|
import com.hippo.ehviewer.ui.tools.dataStateFlow
|
||||||
import com.hippo.ehviewer.util.AppConfig
|
import com.hippo.ehviewer.util.AppConfig
|
||||||
import com.hippo.ehviewer.util.Crash
|
import com.hippo.ehviewer.util.Crash
|
||||||
import com.hippo.ehviewer.util.FavouriteStatusRouter
|
import com.hippo.ehviewer.util.FavouriteStatusRouter
|
||||||
import com.hippo.ehviewer.util.FileUtils
|
import com.hippo.ehviewer.util.FileUtils
|
||||||
import com.hippo.ehviewer.util.ReadableTime
|
|
||||||
import com.hippo.ehviewer.util.isAtLeastP
|
import com.hippo.ehviewer.util.isAtLeastP
|
||||||
import com.hippo.ehviewer.util.isAtLeastQ
|
import com.hippo.ehviewer.util.isAtLeastS
|
||||||
import com.hippo.ehviewer.util.isCronetSupported
|
|
||||||
import eu.kanade.tachiyomi.network.interceptor.CloudflareInterceptor
|
|
||||||
import eu.kanade.tachiyomi.network.interceptor.UncaughtExceptionInterceptor
|
|
||||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
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.plugins.cookies.HttpCookies
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import moe.tarsin.kt.unreachable
|
import logcat.AndroidLogcatLogger
|
||||||
import okhttp3.AsyncDns
|
import logcat.LogPriority
|
||||||
import okhttp3.android.AndroidAsyncDns
|
import logcat.LogcatLogger
|
||||||
|
import logcat.asLog
|
||||||
import okio.Path.Companion.toOkioPath
|
import okio.Path.Companion.toOkioPath
|
||||||
import splitties.arch.room.roomDb
|
import splitties.arch.room.roomDb
|
||||||
import splitties.init.appCtx
|
import splitties.init.appCtx
|
||||||
@@ -64,11 +77,19 @@ import splitties.init.appCtx
|
|||||||
private val lifecycle = ProcessLifecycleOwner.get().lifecycle
|
private val lifecycle = ProcessLifecycleOwner.get().lifecycle
|
||||||
private val lifecycleScope = lifecycle.coroutineScope
|
private val lifecycleScope = lifecycle.coroutineScope
|
||||||
|
|
||||||
class EhApplication : Application(), ImageLoaderFactory {
|
class EhApplication : Application(), SingletonImageLoader.Factory {
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
// Initialize Settings on first access
|
// Initialize Settings on first access
|
||||||
lifecycleScope.launchIO {
|
lifecycleScope.launchIO {
|
||||||
AppCompatDelegate.setDefaultNightMode(Settings.theme)
|
val mode = Settings.theme
|
||||||
|
if (!isAtLeastS) {
|
||||||
|
withUIContext {
|
||||||
|
AppCompatDelegate.setDefaultNightMode(mode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!LogcatLogger.isInstalled && Settings.saveCrashLog) {
|
||||||
|
LogcatLogger.install(AndroidLogcatLogger(LogPriority.VERBOSE))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
lifecycle.addObserver(lockObserver)
|
lifecycle.addObserver(lockObserver)
|
||||||
val handler = Thread.getDefaultUncaughtExceptionHandler()
|
val handler = Thread.getDefaultUncaughtExceptionHandler()
|
||||||
@@ -82,46 +103,51 @@ class EhApplication : Application(), ImageLoaderFactory {
|
|||||||
handler?.uncaughtException(t, e)
|
handler?.uncaughtException(t, e)
|
||||||
}
|
}
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
if (isAtLeastP) {
|
System.loadLibrary("ehviewer")
|
||||||
System.loadLibrary("ehviewer")
|
|
||||||
}
|
|
||||||
System.loadLibrary("ehviewer_rust")
|
|
||||||
ReadableTime.initialize(this)
|
|
||||||
lifecycleScope.launchIO {
|
lifecycleScope.launchIO {
|
||||||
launchIO {
|
launch { EhTagDatabase }
|
||||||
migrateCookies()
|
launch { EhDB }
|
||||||
|
dataStateFlow.value
|
||||||
|
launch {
|
||||||
|
if (DownloadManager.labelList.isNotEmpty() && Settings.downloadFilterMode.key !in Settings.prefs) {
|
||||||
|
Settings.downloadFilterMode.value = DownloadsFilterMode.CUSTOM.flag
|
||||||
|
}
|
||||||
|
DownloadManager.readMetadataFromLocal()
|
||||||
}
|
}
|
||||||
launchIO {
|
launch {
|
||||||
EhTagDatabase
|
FileUtils.cleanupDirectory(AppConfig.externalCrashDir)
|
||||||
|
FileUtils.cleanupDirectory(AppConfig.externalParseErrorDir)
|
||||||
}
|
}
|
||||||
launchIO {
|
launch {
|
||||||
EhDB
|
|
||||||
}
|
|
||||||
launchIO {
|
|
||||||
DownloadManager.isIdle
|
|
||||||
}
|
|
||||||
launchIO {
|
|
||||||
cleanupDownload()
|
cleanupDownload()
|
||||||
}
|
}
|
||||||
if (Settings.requestNews) {
|
if (Settings.requestNews) {
|
||||||
launchIO {
|
launch {
|
||||||
checkDawn()
|
checkDawn()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
launch {
|
||||||
|
cleanObsoleteCache()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
StrictMode.enableDefaults()
|
||||||
|
Snapshot.registerApplyObserver { anies, _ ->
|
||||||
|
logcat(LogPriority.VERBOSE) { anies.toString() }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
cleanObsoleteCache(this)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun cleanupDownload() {
|
private suspend fun cleanupDownload() {
|
||||||
runCatching {
|
runCatching {
|
||||||
keepNoMediaFileStatus()
|
keepNoMediaFileStatus()
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
it.printStackTrace()
|
logcat(it)
|
||||||
}
|
}
|
||||||
runCatching {
|
runCatching {
|
||||||
clearTempDir()
|
clearTempDir()
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
it.printStackTrace()
|
logcat(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,59 +162,52 @@ class EhApplication : Application(), ImageLoaderFactory {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onTrimMemory(level: Int) {
|
override fun newImageLoader(context: Context) = context.imageLoader {
|
||||||
super.onTrimMemory(level)
|
|
||||||
if (level >= ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW) {
|
|
||||||
galleryDetailCache.evictAll()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun newImageLoader() = imageLoader {
|
|
||||||
components {
|
components {
|
||||||
if (isCronetSupported) {
|
serviceLoaderEnabled(false)
|
||||||
callFactory { unreachable() }
|
add(KtorNetworkFetcherFactory { ktorClient })
|
||||||
installCronetHttpUriFetcher()
|
|
||||||
} else {
|
|
||||||
callFactory(baseOkHttpClient)
|
|
||||||
}
|
|
||||||
if (isAtLeastP) {
|
|
||||||
add { result, options, _ -> ImageDecoderDecoder(result.source, options, false) }
|
|
||||||
}
|
|
||||||
add(MergeInterceptor)
|
add(MergeInterceptor)
|
||||||
|
add(DownloadThumbInterceptor)
|
||||||
|
add(CropBorderInterceptor)
|
||||||
|
add(HardwareBitmapInterceptor)
|
||||||
|
if (isAtLeastP) {
|
||||||
|
add(AnimatedImageDecoder.Factory(false))
|
||||||
|
} else {
|
||||||
|
add(GifDecoder.Factory())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
diskCache(imageCache)
|
diskCache { imageCache }
|
||||||
crossfade(300)
|
crossfade(300)
|
||||||
error(R.drawable.image_failed)
|
val drawable = AppCompatResources.getDrawable(appCtx, R.drawable.image_failed)
|
||||||
if (BuildConfig.DEBUG) logger(DebugLogger())
|
if (drawable != null) error(drawable.asCoilImage(true))
|
||||||
respectCacheHeaders(false)
|
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 {
|
companion object {
|
||||||
val baseOkHttpClient by lazy {
|
val ktorClient by lazy {
|
||||||
httpClient {
|
HttpClient(CronetEngine) {
|
||||||
cookieJar(EhCookieStore)
|
install(HttpCookies) {
|
||||||
if (isAtLeastQ) {
|
storage = EhCookieStore
|
||||||
dns(AsyncDns.toDns(AndroidAsyncDns.IPv4, AndroidAsyncDns.IPv6))
|
|
||||||
}
|
}
|
||||||
chunker { alwaysReadResponseBody(false) }
|
|
||||||
addInterceptor(UncaughtExceptionInterceptor())
|
|
||||||
addInterceptor(CloudflareInterceptor(appCtx))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val noRedirectOkHttpClient by lazy {
|
val noRedirectKtorClient by lazy {
|
||||||
httpClient(baseOkHttpClient) {
|
HttpClient(CronetEngine) {
|
||||||
followRedirects(false)
|
followRedirects = false
|
||||||
}
|
install(HttpCookies) {
|
||||||
}
|
storage = EhCookieStore
|
||||||
|
}
|
||||||
// Never use this okhttp client to download large blobs!!!
|
|
||||||
val okHttpClient by lazy {
|
|
||||||
httpClient(baseOkHttpClient) {
|
|
||||||
cache(
|
|
||||||
appCtx.cacheDir.toOkioPath() / "http_cache",
|
|
||||||
20L * 1024L * 1024L,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,11 +17,10 @@ package com.hippo.ehviewer
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.ParcelFileDescriptor
|
|
||||||
import android.os.ParcelFileDescriptor.MODE_READ_ONLY
|
|
||||||
import arrow.fx.coroutines.release
|
import arrow.fx.coroutines.release
|
||||||
import arrow.fx.coroutines.resource
|
import arrow.fx.coroutines.resource
|
||||||
import com.hippo.ehviewer.client.data.BaseGalleryInfo
|
import com.hippo.ehviewer.client.data.BaseGalleryInfo
|
||||||
|
import com.hippo.ehviewer.dao.DownloadArtist
|
||||||
import com.hippo.ehviewer.dao.DownloadDirname
|
import com.hippo.ehviewer.dao.DownloadDirname
|
||||||
import com.hippo.ehviewer.dao.DownloadInfo
|
import com.hippo.ehviewer.dao.DownloadInfo
|
||||||
import com.hippo.ehviewer.dao.DownloadLabel
|
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.Filter
|
||||||
import com.hippo.ehviewer.dao.HistoryInfo
|
import com.hippo.ehviewer.dao.HistoryInfo
|
||||||
import com.hippo.ehviewer.dao.LocalFavoriteInfo
|
import com.hippo.ehviewer.dao.LocalFavoriteInfo
|
||||||
|
import com.hippo.ehviewer.dao.ProgressInfo
|
||||||
import com.hippo.ehviewer.dao.QuickSearch
|
import com.hippo.ehviewer.dao.QuickSearch
|
||||||
|
import com.hippo.ehviewer.dao.Schema17to18
|
||||||
import com.hippo.ehviewer.download.DownloadManager
|
import com.hippo.ehviewer.download.DownloadManager
|
||||||
import com.hippo.ehviewer.util.sendTo
|
import com.hippo.ehviewer.util.sendTo
|
||||||
|
import com.hippo.unifile.UniFile
|
||||||
|
import com.hippo.unifile.asUniFile
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import splitties.arch.room.roomDb
|
import splitties.arch.room.roomDb
|
||||||
|
|
||||||
object EhDB {
|
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) {
|
private suspend fun putGalleryInfo(galleryInfo: BaseGalleryInfo) {
|
||||||
db.galleryDao().upsert(galleryInfo)
|
db.galleryDao().upsert(galleryInfo)
|
||||||
@@ -46,13 +52,22 @@ object EhDB {
|
|||||||
runCatching { db.galleryDao().delete(galleryInfo) }
|
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 {
|
suspend fun getAllDownloadInfo() = db.downloadsDao().joinList().onEach {
|
||||||
if (it.state == DownloadInfo.STATE_WAIT || it.state == DownloadInfo.STATE_DOWNLOAD) {
|
if (it.state == DownloadInfo.STATE_WAIT || it.state == DownloadInfo.STATE_DOWNLOAD) {
|
||||||
it.state = DownloadInfo.STATE_NONE
|
it.state = DownloadInfo.STATE_NONE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun updateDownloadInfo(downloadInfo: List<DownloadInfo>) {
|
suspend fun updateDownloadInfo(downloadInfo: Collection<DownloadInfo>) {
|
||||||
val dao = db.downloadsDao()
|
val dao = db.downloadsDao()
|
||||||
dao.update(downloadInfo.map(DownloadInfo::downloadInfo))
|
dao.update(downloadInfo.map(DownloadInfo::downloadInfo))
|
||||||
}
|
}
|
||||||
@@ -65,17 +80,17 @@ object EhDB {
|
|||||||
suspend fun removeDownloadInfo(downloadInfo: DownloadInfo) {
|
suspend fun removeDownloadInfo(downloadInfo: DownloadInfo) {
|
||||||
val dao = db.downloadsDao()
|
val dao = db.downloadsDao()
|
||||||
dao.delete(downloadInfo.downloadInfo)
|
dao.delete(downloadInfo.downloadInfo)
|
||||||
dao.fill(downloadInfo.position)
|
|
||||||
deleteGalleryInfo(downloadInfo.galleryInfo)
|
deleteGalleryInfo(downloadInfo.galleryInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun randomLocalFav() = db.localFavoritesDao().random()
|
||||||
|
|
||||||
suspend fun removeDownloadInfo(downloadInfo: List<DownloadInfo>) {
|
suspend fun removeDownloadInfo(downloadInfo: List<DownloadInfo>) {
|
||||||
val dao = db.downloadsDao()
|
val dao = db.downloadsDao()
|
||||||
downloadInfo.forEach {
|
downloadInfo.forEach {
|
||||||
dao.delete(it.downloadInfo)
|
dao.delete(it.downloadInfo)
|
||||||
deleteGalleryInfo(it.galleryInfo)
|
deleteGalleryInfo(it.galleryInfo)
|
||||||
}
|
}
|
||||||
dao.fill(downloadInfo.first().position, downloadInfo.size)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getDownloadDirname(gid: Long): String? {
|
suspend fun getDownloadDirname(gid: Long): String? {
|
||||||
@@ -99,6 +114,12 @@ object EhDB {
|
|||||||
dao.insertOrIgnore(downloadDirnameList)
|
dao.insertOrIgnore(downloadDirnameList)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val downloadsCountByLabel
|
||||||
|
get() = db.downloadsDao().countByLabel()
|
||||||
|
|
||||||
|
val downloadsCountByArtist
|
||||||
|
get() = db.downloadsDao().countByArtist()
|
||||||
|
|
||||||
suspend fun getAllDownloadLabelList() = db.downloadLabelDao().list()
|
suspend fun getAllDownloadLabelList() = db.downloadLabelDao().list()
|
||||||
|
|
||||||
suspend fun addDownloadLabel(raw: DownloadLabel): DownloadLabel {
|
suspend fun addDownloadLabel(raw: DownloadLabel): DownloadLabel {
|
||||||
@@ -125,12 +146,20 @@ object EhDB {
|
|||||||
dao.fill(raw.position)
|
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) {
|
suspend fun removeLocalFavorites(galleryInfo: BaseGalleryInfo) {
|
||||||
db.localFavoritesDao().deleteByKey(galleryInfo.gid)
|
db.localFavoritesDao().deleteByKey(galleryInfo.gid)
|
||||||
deleteGalleryInfo(galleryInfo)
|
deleteGalleryInfo(galleryInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun removeLocalFavorites(galleryInfoList: List<BaseGalleryInfo>) {
|
suspend fun removeLocalFavorites(galleryInfoList: Collection<BaseGalleryInfo>) {
|
||||||
galleryInfoList.forEach {
|
galleryInfoList.forEach {
|
||||||
removeLocalFavorites(it)
|
removeLocalFavorites(it)
|
||||||
}
|
}
|
||||||
@@ -146,7 +175,7 @@ object EhDB {
|
|||||||
db.localFavoritesDao().upsert(LocalFavoriteInfo(galleryInfo.gid))
|
db.localFavoritesDao().upsert(LocalFavoriteInfo(galleryInfo.gid))
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun putLocalFavorites(galleryInfoList: List<BaseGalleryInfo>) {
|
suspend fun putLocalFavorites(galleryInfoList: Collection<BaseGalleryInfo>) {
|
||||||
galleryInfoList.forEach {
|
galleryInfoList.forEach {
|
||||||
putLocalFavorites(it)
|
putLocalFavorites(it)
|
||||||
}
|
}
|
||||||
@@ -182,20 +211,22 @@ object EhDB {
|
|||||||
val historyLazyList
|
val historyLazyList
|
||||||
get() = db.historyDao().joinListLazy()
|
get() = db.historyDao().joinListLazy()
|
||||||
|
|
||||||
|
fun searchHistory(keyword: String) = db.historyDao().joinListLazy("*$keyword*")
|
||||||
|
|
||||||
val localFavLazyList
|
val localFavLazyList
|
||||||
get() = db.localFavoritesDao().joinListLazy()
|
get() = db.localFavoritesDao().joinListLazy()
|
||||||
|
|
||||||
val localFavCount: Flow<Int>
|
val localFavCount: Flow<Int>
|
||||||
get() = db.localFavoritesDao().count()
|
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) {
|
suspend fun putHistoryInfo(galleryInfo: BaseGalleryInfo) {
|
||||||
putGalleryInfo(galleryInfo)
|
putGalleryInfo(galleryInfo)
|
||||||
db.historyDao().upsert(HistoryInfo(galleryInfo.gid))
|
db.historyDao().upsert(HistoryInfo(galleryInfo.gid))
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun modifyHistoryInfoFavslotNonRefresh(gid: Long, slot: Int) {
|
suspend fun updateFavoriteSlot(gid: Long, slot: Int) {
|
||||||
val dao = db.galleryDao()
|
val dao = db.galleryDao()
|
||||||
dao.load(gid)?.let {
|
dao.load(gid)?.let {
|
||||||
it.favoriteSlot = slot
|
it.favoriteSlot = slot
|
||||||
@@ -242,82 +273,57 @@ object EhDB {
|
|||||||
db.filterDao().update(filter)
|
db.filterDao().update(filter)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun exportDB(context: Context, uri: Uri) {
|
fun exportDB(context: Context, file: UniFile) {
|
||||||
val ehExportName = "eh.export.db"
|
db.query("PRAGMA wal_checkpoint(FULL)", null).use { it.moveToNext() }
|
||||||
resource {
|
val dbFile = context.getDatabasePath(DB_NAME)
|
||||||
context.deleteDatabase(ehExportName)
|
dbFile.asUniFile() sendTo file
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun importDB(context: Context, uri: Uri) {
|
suspend fun importDB(context: Context, uri: Uri) {
|
||||||
val tempDBName = "tmp.db"
|
val tempDBName = "tmp.db"
|
||||||
resource {
|
resource {
|
||||||
context.deleteDatabase(tempDBName)
|
context.deleteDatabase(tempDBName)
|
||||||
roomDb<EhDatabase>(tempDBName) { createFromInputStream { context.contentResolver.openInputStream(uri) } }
|
roomDb<EhDatabase>(tempDBName) {
|
||||||
|
createFromInputStream { context.contentResolver.openInputStream(uri) }
|
||||||
|
addMigrations(Schema17to18())
|
||||||
|
}
|
||||||
} release {
|
} release {
|
||||||
it.close()
|
it.close()
|
||||||
context.deleteDatabase(tempDBName)
|
context.deleteDatabase(tempDBName)
|
||||||
} use { oldDB ->
|
} 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()
|
val downloadInfoList = oldDB.downloadsDao().joinList().asReversed()
|
||||||
DownloadManager.addDownloadLabel(downloadLabelList)
|
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 {
|
val filterList = oldDB.filterDao().list()
|
||||||
importDownloadDirname(it)
|
val currentFilterList = db.filterDao().list()
|
||||||
}
|
filterList.forEach {
|
||||||
}
|
if (it !in currentFilterList) addFilter(it)
|
||||||
runCatching {
|
|
||||||
val downloadInfoList = oldDB.downloadsDao().joinList().asReversed()
|
|
||||||
DownloadManager.addDownload(downloadInfoList, false)
|
|
||||||
}
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,20 @@
|
|||||||
|
|
||||||
package com.hippo.ehviewer
|
package com.hippo.ehviewer
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisallowComposableCalls
|
||||||
|
import androidx.compose.runtime.MutableState
|
||||||
|
import androidx.compose.runtime.Stable
|
||||||
|
import androidx.compose.runtime.State
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import com.hippo.ehviewer.client.CHROME_USER_AGENT
|
import com.hippo.ehviewer.client.CHROME_USER_AGENT
|
||||||
import com.hippo.ehviewer.client.EhUtils
|
|
||||||
import com.hippo.ehviewer.client.data.FavListUrlBuilder
|
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
|
import java.util.Locale
|
||||||
import kotlin.reflect.KProperty
|
import kotlin.reflect.KProperty
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
@@ -13,9 +24,78 @@ import kotlinx.coroutines.flow.asFlow
|
|||||||
import kotlinx.coroutines.flow.conflate
|
import kotlinx.coroutines.flow.conflate
|
||||||
import kotlinx.coroutines.flow.debounce
|
import kotlinx.coroutines.flow.debounce
|
||||||
import kotlinx.coroutines.flow.flatMapMerge
|
import kotlinx.coroutines.flow.flatMapMerge
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import splitties.preferences.BoolPref
|
||||||
import splitties.preferences.DataStorePreferences
|
import splitties.preferences.DataStorePreferences
|
||||||
|
import splitties.preferences.FloatPref
|
||||||
|
import splitties.preferences.IntPref
|
||||||
|
import splitties.preferences.LongPref
|
||||||
|
import splitties.preferences.PrefDelegate
|
||||||
|
import splitties.preferences.StringOrNullPref
|
||||||
|
import splitties.preferences.StringPref
|
||||||
|
import splitties.preferences.StringSetOrNullPref
|
||||||
|
import splitties.preferences.StringSetPref
|
||||||
import splitties.preferences.edit
|
import splitties.preferences.edit
|
||||||
|
|
||||||
|
@Suppress("IMPLICIT_CAST_TO_ANY", "UNCHECKED_CAST")
|
||||||
|
fun <T> PrefDelegate<T>.getValue(): T = when (this) {
|
||||||
|
is BoolPref -> value
|
||||||
|
is IntPref -> value
|
||||||
|
is FloatPref -> value
|
||||||
|
is LongPref -> value
|
||||||
|
is StringPref -> value
|
||||||
|
is StringOrNullPref -> value
|
||||||
|
is StringSetPref -> value
|
||||||
|
is StringSetOrNullPref -> value
|
||||||
|
} as T
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
fun <T> PrefDelegate<T>.setValue(newValue: T) = when (this) {
|
||||||
|
is BoolPref -> value = newValue as Boolean
|
||||||
|
is IntPref -> value = newValue as Int
|
||||||
|
is FloatPref -> value = newValue as Float
|
||||||
|
is LongPref -> value = newValue as Long
|
||||||
|
is StringPref -> value = newValue as String
|
||||||
|
is StringOrNullPref -> value = newValue as? String
|
||||||
|
is StringSetPref -> value = newValue as Set<String?>
|
||||||
|
is StringSetOrNullPref -> value = newValue as? Set<String?>
|
||||||
|
}
|
||||||
|
|
||||||
|
@Stable
|
||||||
|
@Composable
|
||||||
|
inline fun <R, T> PrefDelegate<T>.collectAsState(crossinline transform: @DisallowComposableCalls (T) -> R): State<R> {
|
||||||
|
val flow = remember { valueFlow().map { transform(it) } }
|
||||||
|
val init = getValue()
|
||||||
|
return flow.collectAsState(transform(init))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Stable
|
||||||
|
@Composable
|
||||||
|
fun <T> PrefDelegate<T>.collectAsState(): State<T> {
|
||||||
|
val flow = remember { valueFlow() }
|
||||||
|
val init = getValue()
|
||||||
|
return flow.collectAsState(init)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Stable
|
||||||
|
@Composable
|
||||||
|
fun <T> PrefDelegate<T>.asMutableState(): MutableState<T> {
|
||||||
|
val flow = remember { valueFlow() }
|
||||||
|
val init = getValue()
|
||||||
|
val readOnly = flow.collectAsState(init)
|
||||||
|
return remember {
|
||||||
|
object : MutableState<T> {
|
||||||
|
override var value: T
|
||||||
|
get() = readOnly.value
|
||||||
|
set(value) {
|
||||||
|
setValue(value)
|
||||||
|
}
|
||||||
|
override fun component1() = TODO()
|
||||||
|
override fun component2() = TODO()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
object Settings : DataStorePreferences(null) {
|
object Settings : DataStorePreferences(null) {
|
||||||
private const val KEY_SHOW_TAG_TRANSLATIONS = "show_tag_translations"
|
private const val KEY_SHOW_TAG_TRANSLATIONS = "show_tag_translations"
|
||||||
|
|
||||||
@@ -25,7 +105,23 @@ object Settings : DataStorePreferences(null) {
|
|||||||
var favCount by intArrayPref("fav_count", 10).emitTo(_favFlow)
|
var favCount by intArrayPref("fav_count", 10).emitTo(_favFlow)
|
||||||
var favCloudCount by intPref("fav_cloud", 0).emitTo(_favFlow)
|
var favCloudCount by intPref("fav_cloud", 0).emitTo(_favFlow)
|
||||||
|
|
||||||
val needSignInFlow: Flow<Boolean>
|
val listMode = intPref("list_mode_2", 0)
|
||||||
|
val detailSize = intPref("detail_size_2", 0)
|
||||||
|
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 meteredNetworkWarning = boolPref("cellular_network_warning", false)
|
||||||
|
val blackDarkTheme = boolPref("black_dark_theme", false)
|
||||||
|
val gridView = boolPref("grid_view", false)
|
||||||
|
val showGalleryPages = boolPref("show_gallery_pages", true)
|
||||||
|
val qSSaveProgress = boolPref("qs_save_progress", true)
|
||||||
|
val security = boolPref("require_unlock", false)
|
||||||
|
val animateItems = boolPref("animate_items", true)
|
||||||
|
val displayName = stringOrNullPref("display_name", null)
|
||||||
|
val recentDownloadLabel = stringOrNullPref("recent_download_label", null)
|
||||||
|
|
||||||
var downloadScheme by stringOrNullPref("image_scheme", null)
|
var downloadScheme by stringOrNullPref("image_scheme", null)
|
||||||
var downloadAuthority by stringOrNullPref("image_authority", null)
|
var downloadAuthority by stringOrNullPref("image_authority", null)
|
||||||
var downloadPath by stringOrNullPref("image_path", null)
|
var downloadPath by stringOrNullPref("image_path", null)
|
||||||
@@ -38,8 +134,6 @@ object Settings : DataStorePreferences(null) {
|
|||||||
var preloadImage by intPref("preload_image_2", 5)
|
var preloadImage by intPref("preload_image_2", 5)
|
||||||
var downloadTimeout by intPref("download_timeout", 60)
|
var downloadTimeout by intPref("download_timeout", 60)
|
||||||
var theme by intPref("theme_2", -1).observed { updateWhenThemeChanges() }
|
var theme by intPref("theme_2", -1).observed { updateWhenThemeChanges() }
|
||||||
var listMode by intPref("list_mode_2", 0)
|
|
||||||
var detailSize by intPref("detail_size_2", 0)
|
|
||||||
var thumbResolution by intPref("thumb_resolution_2", 0)
|
var thumbResolution by intPref("thumb_resolution_2", 0)
|
||||||
var readCacheSize by intPref("read_cache_size_2", 640)
|
var readCacheSize by intPref("read_cache_size_2", 640)
|
||||||
var launchPage by intPref("launch_page_2", 0)
|
var launchPage by intPref("launch_page_2", 0)
|
||||||
@@ -49,44 +143,68 @@ object Settings : DataStorePreferences(null) {
|
|||||||
var requestNews by boolPref("request_news", false).observed { updateWhenRequestNewsChanges() }
|
var requestNews by boolPref("request_news", false).observed { updateWhenRequestNewsChanges() }
|
||||||
var hideHvEvents by boolPref("hide_hv_events", false)
|
var hideHvEvents by boolPref("hide_hv_events", false)
|
||||||
var showJpnTitle by boolPref("show_jpn_title", 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 showTagTranslations by boolPref(KEY_SHOW_TAG_TRANSLATIONS, false).observed { updateWhenTagTranslationChanges() }
|
||||||
var meteredNetworkWarning by boolPref("cellular_network_warning", false)
|
|
||||||
var appLinkVerifyTip by boolPref("app_link_verify_tip", false)
|
var appLinkVerifyTip by boolPref("app_link_verify_tip", false)
|
||||||
var enabledSecurity by boolPref("enable_secure", 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 mediaScan by boolPref("media_scan", false).observed { updateWhenKeepMediaStatusChanges() }
|
||||||
var hasDefaultDownloadLabel by boolPref("has_default_download_label", false)
|
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 saveParseErrorBody by boolPref("save_parse_error_body", true)
|
||||||
var saveCrashLog by boolPref("save_crash_log", true)
|
var saveCrashLog by boolPref("save_crash_log", true)
|
||||||
var security by boolPref("require_unlock", false)
|
|
||||||
var removeImageFiles by boolPref("include_pic", true)
|
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 blackDarkTheme by boolPref("black_dark_theme", false).observed { updateWhenAmoledModeChanges() }
|
|
||||||
var harmonizeCategoryColor by boolPref("harmonize_category_color", true)
|
var harmonizeCategoryColor by boolPref("harmonize_category_color", true)
|
||||||
var preloadThumbAggressively by boolPref("preload_thumb_aggressively", false)
|
var preloadThumbAggressively by boolPref("preload_thumb_aggressively", false)
|
||||||
var downloadOriginImage by boolPref("download_origin_image", 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 thumbSizeDp by intPref("thumb_size_", 120)
|
var archiveMetadata by boolPref("archive_metadata", true)
|
||||||
var recentFavCat by intPref("recent_fav_cat", FavListUrlBuilder.FAV_CAT_LOCAL)
|
var recentFavCat by intPref("recent_fav_cat", FavListUrlBuilder.FAV_CAT_LOCAL)
|
||||||
var defaultFavSlot by intPref("default_favorite_slot", -2)
|
var defaultFavSlot by intPref("default_favorite_slot", -2)
|
||||||
var securityDelay by intPref("require_unlock_delay", 0)
|
var securityDelay by intPref("require_unlock_delay", 0)
|
||||||
var clipboardTextHashCode by intPref("clipboard_text_hash_code", 0)
|
var clipboardTextHashCode by intPref("clipboard_text_hash_code", 0)
|
||||||
var listThumbSize by intPref("list_tile_size", 40)
|
var requestNewsTime by intPref("request_news_time", 0).observed { updateWhenRequestNewsChanges() }
|
||||||
var searchCategory by intPref("search_pref", EhUtils.ALL_CATEGORY)
|
var updateIntervalDays by intPref("update_interval_days", 7)
|
||||||
var requestNewsTimerHour by intPref("request_news_timer_hour", -1)
|
var lastDawnDays by intPref("last_dawn_days", 0)
|
||||||
var requestNewsTimerMinute by intPref("request_news_timer_minute", -1)
|
|
||||||
var updateIntervalDays by intPref("update_interval_days", 0)
|
|
||||||
var recentToplist by stringPref("recent_toplist", "11")
|
var recentToplist by stringPref("recent_toplist", "11")
|
||||||
var userAgent by stringPref("user_agent", CHROME_USER_AGENT)
|
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 defaultDownloadLabel by stringOrNullPref("default_download_label", null)
|
||||||
var displayName by stringOrNullPref("display_name", null)
|
var lastUpdateTime by longPref("last_update_time", BuildConfig.BUILD_TIME)
|
||||||
var avatar by stringOrNullPref("avatar", null)
|
|
||||||
var language by stringPref("app_language", "system").observed { updateWhenLocaleChanges() }
|
// Tachiyomi Reader
|
||||||
var lastDawnDay by longPref("last_dawn_day", 0)
|
var newReader by boolPref("new_compose_reader", false)
|
||||||
var lastUpdateDay by longPref("last_update_day", 0)
|
|
||||||
|
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)
|
||||||
|
val customBrightness = boolPref("pref_custom_brightness_key", false)
|
||||||
|
val customBrightnessValue = intPref("custom_brightness_value", 0)
|
||||||
|
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 {
|
init {
|
||||||
if ("CN" == Locale.getDefault().country) {
|
if ("CN" == Locale.getDefault().country) {
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
package com.hippo.ehviewer
|
package com.hippo.ehviewer
|
||||||
|
|
||||||
|
import android.app.UiModeManager
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
import androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY
|
|
||||||
import androidx.core.os.LocaleListCompat
|
|
||||||
import com.hippo.ehviewer.client.EhCookieStore
|
import com.hippo.ehviewer.client.EhCookieStore
|
||||||
import com.hippo.ehviewer.client.EhEngine
|
import com.hippo.ehviewer.client.EhEngine
|
||||||
import com.hippo.ehviewer.client.EhTagDatabase
|
import com.hippo.ehviewer.client.EhTagDatabase
|
||||||
import com.hippo.ehviewer.dailycheck.updateDailyCheckWork
|
import com.hippo.ehviewer.dailycheck.updateDailyCheckWork
|
||||||
import com.hippo.ehviewer.ui.keepNoMediaFileStatus
|
import com.hippo.ehviewer.ui.keepNoMediaFileStatus
|
||||||
|
import com.hippo.ehviewer.util.isAtLeastS
|
||||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||||
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
@@ -17,6 +18,7 @@ import kotlinx.coroutines.flow.MutableSharedFlow
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import splitties.init.appCtx
|
import splitties.init.appCtx
|
||||||
import splitties.preferences.PrefDelegate
|
import splitties.preferences.PrefDelegate
|
||||||
|
import splitties.systemservices.uiModeManager
|
||||||
|
|
||||||
private val collectScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
private val collectScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
||||||
fun <T, R : PrefDelegate<T>> R.observed(func: (Unit) -> Unit) = apply { collectScope.launch { changesFlow().collect(func) } }
|
fun <T, R : PrefDelegate<T>> R.observed(func: (Unit) -> Unit) = apply { collectScope.launch { changesFlow().collect(func) } }
|
||||||
@@ -24,15 +26,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 : 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 <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() {
|
fun updateWhenKeepMediaStatusChanges() {
|
||||||
collectScope.launchIO {
|
collectScope.launchIO {
|
||||||
runCatching {
|
runCatching {
|
||||||
@@ -44,14 +37,14 @@ fun updateWhenKeepMediaStatusChanges() {
|
|||||||
fun updateWhenThemeChanges() {
|
fun updateWhenThemeChanges() {
|
||||||
collectScope.launch {
|
collectScope.launch {
|
||||||
delay(100) // Avoid recompose being cancelled
|
delay(100) // Avoid recompose being cancelled
|
||||||
AppCompatDelegate.setDefaultNightMode(Settings.theme)
|
if (isAtLeastS) {
|
||||||
}
|
val mode = when (Settings.theme) {
|
||||||
}
|
AppCompatDelegate.MODE_NIGHT_NO -> UiModeManager.MODE_NIGHT_NO
|
||||||
|
AppCompatDelegate.MODE_NIGHT_YES -> UiModeManager.MODE_NIGHT_YES
|
||||||
fun updateWhenAmoledModeChanges() {
|
else -> UiModeManager.MODE_NIGHT_AUTO
|
||||||
collectScope.launch {
|
}
|
||||||
delay(100) // Avoid recompose being cancelled
|
uiModeManager.setApplicationNightMode(mode)
|
||||||
AppCompatDelegate.setDefaultNightMode(MODE_NIGHT_AUTO_BATTERY)
|
}
|
||||||
AppCompatDelegate.setDefaultNightMode(Settings.theme)
|
AppCompatDelegate.setDefaultNightMode(Settings.theme)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -66,7 +59,7 @@ fun updateWhenGallerySiteChanges() {
|
|||||||
runCatching {
|
runCatching {
|
||||||
EhEngine.getUConfig()
|
EhEngine.getUConfig()
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
it.printStackTrace()
|
logcat(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -77,7 +70,7 @@ fun updateWhenTagTranslationChanges() {
|
|||||||
runCatching {
|
runCatching {
|
||||||
EhTagDatabase.update()
|
EhTagDatabase.update()
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
it.printStackTrace()
|
logcat(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,108 +1,85 @@
|
|||||||
/*
|
|
||||||
* Copyright 2016 Hippo Seven
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package com.hippo.ehviewer.client
|
package com.hippo.ehviewer.client
|
||||||
|
|
||||||
import android.webkit.CookieManager
|
import android.webkit.CookieManager
|
||||||
import androidx.compose.ui.util.fastAny
|
import androidx.compose.ui.util.fastAny
|
||||||
import androidx.compose.ui.util.fastFilter
|
|
||||||
import androidx.compose.ui.util.fastForEach
|
|
||||||
import androidx.compose.ui.util.fastJoinToString
|
import androidx.compose.ui.util.fastJoinToString
|
||||||
import androidx.compose.ui.util.fastMapNotNull
|
import io.ktor.client.plugins.cookies.CookiesStorage
|
||||||
import okhttp3.Cookie
|
import io.ktor.http.Cookie
|
||||||
import okhttp3.CookieJar
|
import io.ktor.http.Url
|
||||||
import okhttp3.HttpUrl
|
import io.ktor.http.parseClientCookiesHeader
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
import io.ktor.http.renderCookieHeader
|
||||||
|
import io.ktor.http.renderSetCookieHeader
|
||||||
|
|
||||||
object EhCookieStore : CookieJar {
|
object EhCookieStore : CookiesStorage {
|
||||||
private val manager = CookieManager.getInstance()
|
private val manager = CookieManager.getInstance()
|
||||||
fun signOut() = manager.removeAllCookies(null)
|
fun signOut() = manager.removeAllCookies(null)
|
||||||
fun contains(url: HttpUrl, name: String) = get(url).fastAny { it.name == name }
|
fun contains(url: String, name: String) = load(Url(url)).fastAny { it.name == name }
|
||||||
|
|
||||||
fun get(url: HttpUrl): List<Cookie> {
|
|
||||||
val cookies = manager.getCookie(url.toString())
|
|
||||||
|
|
||||||
return if (!cookies.isNullOrEmpty()) {
|
|
||||||
cookies.split(";").fastMapNotNull { setCookie ->
|
|
||||||
Cookie.parse(url, setCookie)?.takeUnless { it.name == KEY_UTMP_NAME }
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
emptyList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun hasSignedIn(): Boolean {
|
fun hasSignedIn(): Boolean {
|
||||||
val url = EhUrl.HOST_E.toHttpUrl()
|
val url = EhUrl.HOST_E
|
||||||
return contains(url, KEY_IPB_MEMBER_ID) && contains(url, KEY_IPB_PASS_HASH)
|
return contains(url, KEY_IPB_MEMBER_ID) && contains(url, KEY_IPB_PASS_HASH)
|
||||||
}
|
}
|
||||||
|
|
||||||
const val KEY_IPB_MEMBER_ID = "ipb_member_id"
|
const val KEY_IPB_MEMBER_ID = "ipb_member_id"
|
||||||
const val KEY_IPB_PASS_HASH = "ipb_pass_hash"
|
const val KEY_IPB_PASS_HASH = "ipb_pass_hash"
|
||||||
const val KEY_IGNEOUS = "igneous"
|
const val KEY_IGNEOUS = "igneous"
|
||||||
private const val KEY_STAR = "star"
|
|
||||||
private const val KEY_CONTENT_WARNING = "nw"
|
private const val KEY_CONTENT_WARNING = "nw"
|
||||||
private const val CONTENT_WARNING_NOT_SHOW = "1"
|
private const val CONTENT_WARNING_NOT_SHOW = "1"
|
||||||
private const val KEY_UTMP_NAME = "__utmp"
|
private const val KEY_UTMP_NAME = "__utmp"
|
||||||
private val sTipsCookie = Cookie.Builder().apply {
|
private val sTipsCookie = Cookie(
|
||||||
name(KEY_CONTENT_WARNING)
|
name = KEY_CONTENT_WARNING,
|
||||||
value(CONTENT_WARNING_NOT_SHOW)
|
value = CONTENT_WARNING_NOT_SHOW,
|
||||||
domain(EhUrl.DOMAIN_E)
|
)
|
||||||
path("/")
|
|
||||||
expiresAt(Long.MAX_VALUE)
|
|
||||||
}.build()
|
|
||||||
|
|
||||||
fun copyNecessaryCookies() {
|
fun clearIgneous() {
|
||||||
val cookies = get(EhUrl.HOST_E.toHttpUrl())
|
manager.setCookie(
|
||||||
cookies.fastForEach {
|
EhUrl.HOST_EX,
|
||||||
if (it.name == KEY_STAR || it.name == KEY_IPB_MEMBER_ID || it.name == KEY_IPB_PASS_HASH) {
|
renderSetCookieHeader(KEY_IGNEOUS, "", maxAge = 0, domain = EhUrl.DOMAIN_EX, path = "/"),
|
||||||
manager.setCookie(EhUrl.HOST_EX, it.toString())
|
)
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteCookie(url: HttpUrl, name: String) {
|
fun getIdentityCookies(): List<Pair<String, String?>> {
|
||||||
manager.setCookie(url.toString(), "$name=;Max-Age=0")
|
val eCookies = getCookies(EhUrl.HOST_E)
|
||||||
}
|
val exCookies = getCookies(EhUrl.HOST_EX)
|
||||||
|
val ipbMemberId = eCookies?.get(KEY_IPB_MEMBER_ID)
|
||||||
fun addCookie(cookie: Cookie) {
|
val ipbPassHash = eCookies?.get(KEY_IPB_PASS_HASH)
|
||||||
manager.setCookie(if (EhUrl.DOMAIN_E == cookie.domain) EhUrl.HOST_E else EhUrl.HOST_EX, cookie.toString())
|
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 flush() = manager.flush()
|
||||||
|
|
||||||
fun getCookieHeader(url: HttpUrl): String {
|
fun getCookieHeader(url: String): String {
|
||||||
return loadForRequest(url).fastJoinToString("; ") {
|
return load(Url(url)).fastJoinToString("; ") {
|
||||||
"${it.name}=${it.value}"
|
renderCookieHeader(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// See https://github.com/Ehviewer-Overhauled/Ehviewer/issues/873
|
// See https://github.com/Ehviewer-Overhauled/Ehviewer/issues/873
|
||||||
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) = cookies.fastForEach {
|
override suspend fun addCookie(requestUrl: Url, cookie: Cookie) {
|
||||||
if (it.name != KEY_UTMP_NAME) {
|
if (cookie.name != KEY_UTMP_NAME) {
|
||||||
manager.setCookie(url.toString(), it.toString())
|
manager.setCookie(requestUrl.toString(), renderSetCookieHeader(cookie))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun loadForRequest(url: HttpUrl): List<Cookie> {
|
override fun close() = Unit
|
||||||
|
|
||||||
|
private fun getCookies(url: String) = manager.getCookie(url)?.let { parseClientCookiesHeader(it) }
|
||||||
|
|
||||||
|
fun load(url: Url): List<Cookie> {
|
||||||
val checkTips = EhUrl.DOMAIN_E in url.host
|
val checkTips = EhUrl.DOMAIN_E in url.host
|
||||||
return get(url).run {
|
return getCookies(url.toString())?.mapTo(mutableListOf()) {
|
||||||
|
Cookie(it.key, it.value)
|
||||||
|
}?.apply {
|
||||||
if (checkTips) {
|
if (checkTips) {
|
||||||
fastFilter { it.name != KEY_CONTENT_WARNING }.toMutableList().apply { add(sTipsCookie) }
|
add(sTipsCookie)
|
||||||
} else {
|
|
||||||
this
|
|
||||||
}
|
}
|
||||||
}
|
} ?: if (checkTips) listOf(sTipsCookie) else emptyList()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun get(requestUrl: Url) = load(requestUrl)
|
||||||
}
|
}
|
||||||
@@ -15,12 +15,17 @@
|
|||||||
*/
|
*/
|
||||||
package com.hippo.ehviewer.client
|
package com.hippo.ehviewer.client
|
||||||
|
|
||||||
import android.util.Log
|
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.parMap
|
||||||
import arrow.fx.coroutines.parZip
|
import arrow.fx.coroutines.parZip
|
||||||
|
import com.hippo.ehviewer.EhDB
|
||||||
import com.hippo.ehviewer.R
|
import com.hippo.ehviewer.R
|
||||||
import com.hippo.ehviewer.Settings
|
import com.hippo.ehviewer.Settings
|
||||||
import com.hippo.ehviewer.client.data.BaseGalleryInfo
|
import com.hippo.ehviewer.client.data.BaseGalleryInfo
|
||||||
|
import com.hippo.ehviewer.client.data.FavListUrlBuilder
|
||||||
import com.hippo.ehviewer.client.data.GalleryInfo
|
import com.hippo.ehviewer.client.data.GalleryInfo
|
||||||
import com.hippo.ehviewer.client.exception.EhException
|
import com.hippo.ehviewer.client.exception.EhException
|
||||||
import com.hippo.ehviewer.client.exception.InsufficientFundsException
|
import com.hippo.ehviewer.client.exception.InsufficientFundsException
|
||||||
@@ -28,13 +33,13 @@ import com.hippo.ehviewer.client.exception.NotLoggedInException
|
|||||||
import com.hippo.ehviewer.client.exception.ParseException
|
import com.hippo.ehviewer.client.exception.ParseException
|
||||||
import com.hippo.ehviewer.client.parser.ArchiveParser
|
import com.hippo.ehviewer.client.parser.ArchiveParser
|
||||||
import com.hippo.ehviewer.client.parser.EventPaneParser
|
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.FavoritesParser
|
||||||
import com.hippo.ehviewer.client.parser.ForumsParser
|
import com.hippo.ehviewer.client.parser.ForumsParser
|
||||||
import com.hippo.ehviewer.client.parser.GalleryApiParser
|
import com.hippo.ehviewer.client.parser.GalleryApiParser
|
||||||
import com.hippo.ehviewer.client.parser.GalleryDetailParser
|
import com.hippo.ehviewer.client.parser.GalleryDetailParser
|
||||||
import com.hippo.ehviewer.client.parser.GalleryListParser
|
import com.hippo.ehviewer.client.parser.GalleryListParser
|
||||||
import com.hippo.ehviewer.client.parser.GalleryNotAvailableParser
|
import com.hippo.ehviewer.client.parser.GalleryNotAvailableParser
|
||||||
import com.hippo.ehviewer.client.parser.GalleryPageApiParser
|
|
||||||
import com.hippo.ehviewer.client.parser.GalleryPageParser
|
import com.hippo.ehviewer.client.parser.GalleryPageParser
|
||||||
import com.hippo.ehviewer.client.parser.GalleryTokenApiParser
|
import com.hippo.ehviewer.client.parser.GalleryTokenApiParser
|
||||||
import com.hippo.ehviewer.client.parser.HomeParser
|
import com.hippo.ehviewer.client.parser.HomeParser
|
||||||
@@ -43,47 +48,64 @@ import com.hippo.ehviewer.client.parser.RateGalleryResult
|
|||||||
import com.hippo.ehviewer.client.parser.SignInParser
|
import com.hippo.ehviewer.client.parser.SignInParser
|
||||||
import com.hippo.ehviewer.client.parser.TorrentParser
|
import com.hippo.ehviewer.client.parser.TorrentParser
|
||||||
import com.hippo.ehviewer.client.parser.TorrentResult
|
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.VoteCommentResult
|
||||||
import com.hippo.ehviewer.client.parser.VoteTagParser
|
import com.hippo.ehviewer.client.parser.VoteTagParser
|
||||||
import com.hippo.ehviewer.cronet.awaitBodyFully
|
|
||||||
import com.hippo.ehviewer.cronet.cronetRequest
|
|
||||||
import com.hippo.ehviewer.cronet.execute
|
|
||||||
import com.hippo.ehviewer.dailycheck.showEventNotification
|
import com.hippo.ehviewer.dailycheck.showEventNotification
|
||||||
import com.hippo.ehviewer.dailycheck.today
|
import com.hippo.ehviewer.dailycheck.today
|
||||||
import com.hippo.ehviewer.util.AppConfig
|
import com.hippo.ehviewer.util.AppConfig
|
||||||
|
import com.hippo.ehviewer.util.ReadableTime
|
||||||
import com.hippo.ehviewer.util.StatusCodeException
|
import com.hippo.ehviewer.util.StatusCodeException
|
||||||
import com.hippo.ehviewer.util.isCronetSupported
|
import com.hippo.ehviewer.util.bodyAsUtf8Text
|
||||||
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
|
import io.ktor.client.statement.HttpStatement
|
||||||
|
import io.ktor.client.statement.bodyAsChannel
|
||||||
import io.ktor.utils.io.pool.DirectByteBufferPool
|
import io.ktor.utils.io.pool.DirectByteBufferPool
|
||||||
import io.ktor.utils.io.pool.useInstance
|
import io.ktor.utils.io.pool.useInstance
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
import kotlin.math.ceil
|
import kotlin.math.ceil
|
||||||
|
import kotlin.system.measureTimeMillis
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.serialization.SerializationException
|
||||||
import kotlinx.serialization.json.add
|
import kotlinx.serialization.json.add
|
||||||
import kotlinx.serialization.json.addJsonArray
|
import kotlinx.serialization.json.addJsonArray
|
||||||
import kotlinx.serialization.json.put
|
import kotlinx.serialization.json.put
|
||||||
import moe.tarsin.coroutines.removeAllSuspend
|
import moe.tarsin.coroutines.removeAllSuspend
|
||||||
import moe.tarsin.coroutines.runSuspendCatching
|
import moe.tarsin.coroutines.runSuspendCatching
|
||||||
import okhttp3.MediaType
|
import okio.buffer
|
||||||
import okhttp3.MediaType.Companion.toMediaType
|
import okio.sink
|
||||||
import okhttp3.MultipartBody
|
|
||||||
import okhttp3.Request
|
|
||||||
import okhttp3.RequestBody.Companion.asRequestBody
|
|
||||||
import org.jsoup.Jsoup
|
import org.jsoup.Jsoup
|
||||||
import splitties.init.appCtx
|
import splitties.init.appCtx
|
||||||
|
|
||||||
private const val TAG = "EhEngine"
|
// https://ehwiki.org/wiki/API#Basics
|
||||||
private const val MAX_REQUEST_SIZE = 25
|
private const val MAX_REQUEST_SIZE = 25
|
||||||
private const val U_CONFIG_TEXT = "Selected Profile"
|
private const val MAX_SEQUENTIAL_REQUESTS = 5
|
||||||
private val MEDIA_TYPE_JPEG: MediaType = "image/jpeg".toMediaType()
|
private const val REQUEST_INTERVAL = 5000L
|
||||||
|
|
||||||
private fun rethrowExactly(code: Int, body: String, e: Throwable): Nothing {
|
fun Either<String, ByteBuffer>.saveParseError(e: Throwable) {
|
||||||
|
val dir = AppConfig.externalParseErrorDir ?: return
|
||||||
|
val file = File(dir, ReadableTime.getFilenamableTime(System.currentTimeMillis()) + ".txt")
|
||||||
|
file.sink().buffer().use { sink ->
|
||||||
|
with(sink) {
|
||||||
|
writeUtf8(e.message + "\n")
|
||||||
|
onLeft { writeUtf8(it) }
|
||||||
|
onRight { write(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun rethrowExactly(code: Int, body: Either<String, ByteBuffer>, e: Throwable): Nothing {
|
||||||
// Don't translate coroutine cancellation
|
// Don't translate coroutine cancellation
|
||||||
if (e is CancellationException) throw e
|
if (e is CancellationException) throw e
|
||||||
|
|
||||||
// Check sad panda(without panda)
|
// Check sad panda (without panda)
|
||||||
if (body.isEmpty()) {
|
val empty = body.fold(
|
||||||
|
{ it.isEmpty() },
|
||||||
|
{ !it.hasRemaining() },
|
||||||
|
)
|
||||||
|
if (empty) {
|
||||||
if (EhUtils.isExHentai) {
|
if (EhUtils.isExHentai) {
|
||||||
throw EhException("Sad Panda\n(without panda)")
|
throw EhException("Sad Panda\n(without panda)")
|
||||||
} else {
|
} else {
|
||||||
@@ -92,10 +114,12 @@ private fun rethrowExactly(code: Int, body: String, e: Throwable): Nothing {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check Gallery Not Available
|
// Check Gallery Not Available
|
||||||
if (body.contains("Gallery Not Available - ")) {
|
body.onLeft {
|
||||||
val error = GalleryNotAvailableParser.parse(body)
|
if ("Gallery Not Available - " in it) {
|
||||||
if (!error.isNullOrBlank()) {
|
val error = GalleryNotAvailableParser.parse(it)
|
||||||
throw EhException(error)
|
if (!error.isNullOrBlank()) {
|
||||||
|
throw EhException(error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,136 +128,117 @@ private fun rethrowExactly(code: Int, body: String, e: Throwable): Nothing {
|
|||||||
throw StatusCodeException(code)
|
throw StatusCodeException(code)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e is ParseException) {
|
if (e is ParseException || e is SerializationException) {
|
||||||
if ("<" !in body) {
|
body.onLeft { if ("<" !in it) throw EhException(it) }
|
||||||
throw EhException(body)
|
when (val message = e.cause?.message) {
|
||||||
} else {
|
"No hits found!" -> throw EhException(R.string.gallery_list_empty_hit)
|
||||||
if (Settings.saveParseErrorBody) AppConfig.saveParseErrorBody(e, body)
|
"No watched tags!" -> throw EhException(R.string.gallery_list_empty_hit_subscription)
|
||||||
throw EhException(appCtx.getString(R.string.error_parse_error), e)
|
"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)
|
||||||
}
|
}
|
||||||
|
|
||||||
// We can't translate it, rethrow it anyway
|
// We can't translate it, rethrow it anyway
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
|
|
||||||
val httpContentPool = DirectByteBufferPool(16, 0x20000)
|
val httpContentPool = DirectByteBufferPool(8, 0x80000)
|
||||||
|
|
||||||
suspend inline fun <T> fetchCompat(url: String, referer: String? = null, crossinline parser: (ByteBuffer) -> T): T {
|
suspend inline fun <T> HttpStatement.fetchUsingAsText(crossinline block: suspend String.() -> T) = executeSafely { response ->
|
||||||
return if (isCronetSupported) {
|
val body = response.bodyAsUtf8Text()
|
||||||
cronetRequest(url, referer).execute {
|
runSuspendCatching {
|
||||||
httpContentPool.useInstance { buffer ->
|
block(body)
|
||||||
awaitBodyFully {
|
}.onFailure {
|
||||||
buffer.put(it)
|
rethrowExactly(response.status.value, body.left(), it)
|
||||||
}
|
}.getOrThrow()
|
||||||
buffer.flip()
|
|
||||||
parser(buffer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ehRequest(url, referer).execute {
|
|
||||||
httpContentPool.useInstance { buffer ->
|
|
||||||
while (true) {
|
|
||||||
if (body.source().read(buffer) == -1) break
|
|
||||||
}
|
|
||||||
buffer.flip()
|
|
||||||
parser(buffer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("REDUNDANT_INLINE_SUSPEND_FUNCTION_TYPE")
|
suspend inline fun <T> HttpStatement.fetchUsingAsByteBuffer(crossinline block: suspend ByteBuffer.() -> T) = executeSafely { response ->
|
||||||
private suspend inline fun <T> Request.executeAndParsingWith(block: suspend String.() -> T): T {
|
httpContentPool.useInstance { buffer ->
|
||||||
Log.d(TAG, url.toString())
|
with(response.bodyAsChannel()) { while (!isClosedForRead) readAvailable(buffer) }
|
||||||
return execute {
|
buffer.flip()
|
||||||
val body = body.string()
|
runSuspendCatching {
|
||||||
try {
|
block(buffer)
|
||||||
block(body)
|
}.onFailure {
|
||||||
} catch (e: Exception) {
|
buffer.rewind()
|
||||||
rethrowExactly(code, body, e)
|
rethrowExactly(response.status.value, buffer.right(), it)
|
||||||
}
|
}.getOrThrow()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
object EhEngine {
|
object EhEngine {
|
||||||
suspend fun getOriginalImageUrl(url: String, referer: String?): String {
|
suspend fun getOriginalImageUrl(url: String, referer: String?) = noRedirectEhRequest(url, referer).executeSafely { response ->
|
||||||
Log.d(TAG, url)
|
val location = response.headers["Location"] ?: throw InsufficientFundsException()
|
||||||
return ehRequest(url, referer).executeNoRedirect {
|
location.takeIf { "bounce_login" !in it } ?: throw NotLoggedInException()
|
||||||
header("Location")?.apply {
|
|
||||||
if (contains("bounce_login")) {
|
|
||||||
throw NotLoggedInException()
|
|
||||||
}
|
|
||||||
} ?: throw InsufficientFundsException()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
val referer = EhUrl.getGalleryDetailUrl(gid, token)
|
||||||
return fetchCompat(url, referer, TorrentParser::parse)
|
return ehRequest(url, referer).fetchUsingAsByteBuffer(TorrentParser::parse)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getArchiveList(url: String, gid: Long, token: String?) = ehRequest(url, EhUrl.getGalleryDetailUrl(gid, token))
|
suspend fun getArchiveList(url: String, gid: Long, token: String): ArchiveParser.Result {
|
||||||
.executeAndParsingWith(ArchiveParser::parse)
|
val funds = if (EhUtils.isExHentai) getFunds() else null
|
||||||
.apply { funds = funds ?: ehRequest(EhUrl.URL_FUNDS).executeAndParsingWith(HomeParser::parseFunds) }
|
return ehRequest(url, EhUrl.getGalleryDetailUrl(gid, token))
|
||||||
|
.fetchUsingAsText(ArchiveParser::parse.partially2(funds))
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun getImageLimits() = parZip(
|
suspend fun getImageLimits() = parZip(
|
||||||
{ fetchCompat(EhUrl.URL_HOME, parser = HomeParser::parse) },
|
{ ehRequest(EhUrl.URL_HOME).fetchUsingAsByteBuffer(HomeParser::parse) },
|
||||||
{ ehRequest(EhUrl.URL_FUNDS).executeAndParsingWith(HomeParser::parseFunds) },
|
{ getFunds() },
|
||||||
{ limits, funds -> HomeParser.Result(limits, funds) },
|
{ 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)
|
suspend fun getNews(parse: Boolean) = ehRequest(EhUrl.URL_NEWS, EhUrl.REFERER_E)
|
||||||
.executeAndParsingWith { if (parse) EventPaneParser.parse(this) else null }
|
.fetchUsingAsText { if (parse) EventPaneParser.parse(this) else null }
|
||||||
|
|
||||||
suspend fun getProfile(): ProfileParser.Result {
|
suspend fun getProfile(): ProfileParser.Result {
|
||||||
val url = ehRequest(EhUrl.URL_FORUMS).executeAndParsingWith(ForumsParser::parse)
|
val url = ehRequest(EhUrl.URL_FORUMS).fetchUsingAsText(ForumsParser::parse)
|
||||||
return ehRequest(url, EhUrl.URL_FORUMS).executeAndParsingWith(ProfileParser::parse)
|
return ehRequest(url, EhUrl.URL_FORUMS).fetchUsingAsText(ProfileParser::parse)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getUConfig(url: String = EhUrl.uConfigUrl) {
|
suspend fun getUConfig(url: String = EhUrl.uConfigUrl) {
|
||||||
runSuspendCatching {
|
runSuspendCatching {
|
||||||
ehRequest(url).executeAndParsingWith { check(contains(U_CONFIG_TEXT)) { "Unable to load config from $url!" } }
|
ehRequest(url).fetchUsingAsByteBuffer(UserConfigParser::parse)
|
||||||
}.onFailure {
|
}.onFailure { throwable ->
|
||||||
// It may get redirected when accessing ex for the first time
|
// It may get redirected when accessing ex for the first time
|
||||||
if (url == EhUrl.URL_UCONFIG_EX) {
|
if (url == EhUrl.URL_UCONFIG_EX) {
|
||||||
it.printStackTrace()
|
logcat(throwable)
|
||||||
ehRequest(url).executeAndParsingWith { check(contains(U_CONFIG_TEXT)) { "Unable to load config from $url!" } }
|
ehRequest(url).fetchUsingAsByteBuffer(UserConfigParser::parse)
|
||||||
} else {
|
} else {
|
||||||
throw it
|
throw throwable
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getGalleryPage(
|
suspend fun getGalleryPage(url: String, gid: Long, token: String): GalleryPageParser.Result {
|
||||||
url: String,
|
|
||||||
gid: Long,
|
|
||||||
token: String?,
|
|
||||||
): GalleryPageParser.Result {
|
|
||||||
val referer = EhUrl.getGalleryDetailUrl(gid, token)
|
val referer = EhUrl.getGalleryDetailUrl(gid, token)
|
||||||
return ehRequest(url, referer).executeAndParsingWith(GalleryPageParser::parse)
|
return ehRequest(url, referer).fetchUsingAsText(GalleryPageParser::parse)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getGalleryList(url: String) = ehRequest(url, EhUrl.referer)
|
suspend fun getGalleryList(url: String) = ehRequest(url, EhUrl.referer).fetchUsingAsByteBuffer(GalleryListParser::parse)
|
||||||
.executeAndParsingWith(GalleryListParser::parse)
|
.apply { galleryInfoList.fillInfo(url, true) }
|
||||||
.apply { fillGalleryList(galleryInfoList, url, true) }
|
|
||||||
.takeUnless { it.galleryInfoList.isEmpty() } ?: GalleryListParser.emptyResult
|
.takeUnless { it.galleryInfoList.isEmpty() } ?: GalleryListParser.emptyResult
|
||||||
|
|
||||||
suspend fun getGalleryDetail(url: String) = ehRequest(url, EhUrl.referer).executeAndParsingWith {
|
suspend fun getGalleryDetail(url: String) = ehRequest(url, EhUrl.referer).fetchUsingAsText {
|
||||||
EventPaneParser.parse(this)?.let {
|
val eventPane = EventPaneParser.parse(this)
|
||||||
Settings.lastDawnDay = today
|
if (eventPane != null) {
|
||||||
showEventNotification(it)
|
Settings.lastDawnDays = today
|
||||||
|
showEventNotification(eventPane)
|
||||||
}
|
}
|
||||||
GalleryDetailParser.parse(this)
|
GalleryDetailParser.parse(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getPreviewList(url: String) = ehRequest(url, EhUrl.referer).executeAndParsingWith {
|
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)
|
suspend fun getFavorites(url: String) = ehRequest(url, EhUrl.referer).fetchUsingAsByteBuffer(FavoritesParser::parse)
|
||||||
.executeAndParsingWith(FavoritesParser::parse)
|
.apply { galleryInfoList.fillInfo(url) }
|
||||||
.apply { fillGalleryList(galleryInfoList, url) }
|
|
||||||
|
|
||||||
suspend fun signIn(username: String, password: String): String {
|
suspend fun signIn(username: String, password: String): String {
|
||||||
val referer = "https://forums.e-hentai.org/index.php?act=Login&CODE=00"
|
val referer = "https://forums.e-hentai.org/index.php?act=Login&CODE=00"
|
||||||
@@ -241,99 +246,81 @@ object EhEngine {
|
|||||||
val origin = "https://forums.e-hentai.org"
|
val origin = "https://forums.e-hentai.org"
|
||||||
return ehRequest(url, referer, origin) {
|
return ehRequest(url, referer, origin) {
|
||||||
formBody {
|
formBody {
|
||||||
add("referer", referer)
|
append("referer", referer)
|
||||||
add("b", "")
|
append("b", "")
|
||||||
add("bt", "")
|
append("bt", "")
|
||||||
add("UserName", username)
|
append("UserName", username)
|
||||||
add("PassWord", password)
|
append("PassWord", password)
|
||||||
add("CookieDate", "1")
|
append("CookieDate", "1")
|
||||||
}
|
}
|
||||||
}.executeAndParsingWith(SignInParser::parse)
|
}.fetchUsingAsText(SignInParser::parse)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun commentGallery(url: String, comment: String, id: String?) = ehRequest(url, url, EhUrl.origin) {
|
suspend fun commentGallery(url: String, comment: String, id: Long = -1) = ehRequest(url, url, EhUrl.origin) {
|
||||||
formBody {
|
formBody {
|
||||||
if (id == null) {
|
if (id == -1L) {
|
||||||
add("commenttext_new", comment)
|
append("commenttext_new", comment)
|
||||||
} else {
|
} else {
|
||||||
add("commenttext_edit", comment)
|
append("commenttext_edit", comment)
|
||||||
add("edit_comment", id)
|
append("edit_comment", id.toString())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.executeAndParsingWith {
|
}.executeSafely { response ->
|
||||||
val document = Jsoup.parse(this)
|
// Ktor does not handle POST redirect, we need to do it manually
|
||||||
val elements = document.select("#chd + p")
|
// https://youtrack.jetbrains.com/issue/KTOR-478
|
||||||
if (elements.size > 0) {
|
val location = response.headers["Location"] ?: url
|
||||||
throw EhException(elements[0].text())
|
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 = "") {
|
||||||
* @param dstCat -1 for delete, 0 - 9 for cloud favorite, others throw Exception
|
|
||||||
* @param note max 250 characters
|
|
||||||
*/
|
|
||||||
suspend fun modifyFavorites(
|
|
||||||
gid: Long,
|
|
||||||
token: String?,
|
|
||||||
dstCat: Int = -1,
|
|
||||||
note: String = "",
|
|
||||||
) {
|
|
||||||
val catStr: String = when (dstCat) {
|
val catStr: String = when (dstCat) {
|
||||||
-1 -> "favdel"
|
-1 -> "favdel"
|
||||||
in 0..9 -> dstCat.toString()
|
in 0..9 -> dstCat.toString()
|
||||||
else -> throw EhException("Invalid dstCat: $dstCat")
|
else -> throw EhException("Invalid dstCat: $dstCat")
|
||||||
}
|
}
|
||||||
val url = EhUrl.getAddFavorites(gid, token)
|
val url = EhUrl.getAddFavorites(gid, token)
|
||||||
return ehRequest(url, url, EhUrl.origin) {
|
ehRequest(url, url, EhUrl.origin) {
|
||||||
formBody {
|
formBody {
|
||||||
add("favcat", catStr)
|
append("favcat", catStr)
|
||||||
add("favnote", note)
|
append("favnote", note)
|
||||||
// apply=Add+to+Favorites is not necessary, just use apply=Apply+Changes all the time
|
// apply=Add+to+Favorites is not necessary, just use apply=Apply+Changes all the time
|
||||||
add("apply", "Apply Changes")
|
append("apply", "Apply Changes")
|
||||||
add("update", "1")
|
append("update", "1")
|
||||||
}
|
}
|
||||||
}.executeAndParsingWith { }
|
}.executeSafely { }
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun downloadArchive(
|
suspend fun downloadArchive(gid: Long, token: String, or: String, res: String, isHAtH: Boolean): String? {
|
||||||
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")
|
|
||||||
}
|
|
||||||
val url = EhUrl.getDownloadArchive(gid, token, or)
|
val url = EhUrl.getDownloadArchive(gid, token, or)
|
||||||
val referer = EhUrl.getGalleryDetailUrl(gid, token)
|
val referer = EhUrl.getGalleryDetailUrl(gid, token)
|
||||||
val request = ehRequest(url, referer, EhUrl.origin) {
|
val request = ehRequest(url, referer, EhUrl.origin) {
|
||||||
formBody {
|
formBody {
|
||||||
if (isHAtH) {
|
if (isHAtH) {
|
||||||
add("hathdl_xres", res)
|
append("hathdl_xres", res)
|
||||||
} else {
|
} else {
|
||||||
add("dltype", res)
|
append("dltype", res)
|
||||||
if (res == "org") {
|
if (res == "org") {
|
||||||
add("dlcheck", "Download Original Archive")
|
append("dlcheck", "Download Original Archive")
|
||||||
} else {
|
} else {
|
||||||
add("dlcheck", "Download Resample Archive")
|
append("dlcheck", "Download Resample Archive")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var result = request.executeAndParsingWith(ArchiveParser::parseArchiveUrl)
|
var result = request.fetchUsingAsText(ArchiveParser::parseArchiveUrl)
|
||||||
if (!isHAtH) {
|
if (!isHAtH) {
|
||||||
if (result == null) {
|
if (result == null) {
|
||||||
// Wait for the server to prepare archives
|
// Wait for the server to prepare archives
|
||||||
delay(1000)
|
delay(1000)
|
||||||
result = request.executeAndParsingWith(ArchiveParser::parseArchiveUrl)
|
result = request.fetchUsingAsText(ArchiveParser::parseArchiveUrl)
|
||||||
if (result == null) {
|
if (result == null) throw EhException("Archive unavailable")
|
||||||
throw EhException("Archive unavailable")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
@@ -342,11 +329,14 @@ object EhEngine {
|
|||||||
|
|
||||||
suspend fun resetImageLimits() = ehRequest(EhUrl.URL_HOME) {
|
suspend fun resetImageLimits() = ehRequest(EhUrl.URL_HOME) {
|
||||||
formBody {
|
formBody {
|
||||||
add("reset_imagelimit", "Reset Limit")
|
append("reset_imagelimit", "Reset Limit")
|
||||||
}
|
}
|
||||||
}.executeAndParsingWith(HomeParser::parseResetLimits)
|
}.fetchUsingAsText(HomeParser::parseResetLimits)
|
||||||
|
|
||||||
suspend fun modifyFavorites(url: String, gidArray: LongArray, 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()
|
||||||
val catStr: String = when (dstCat) {
|
val catStr: String = when (dstCat) {
|
||||||
-1 -> "delete"
|
-1 -> "delete"
|
||||||
in 0..9 -> "fav$dstCat"
|
in 0..9 -> "fav$dstCat"
|
||||||
@@ -354,19 +344,13 @@ object EhEngine {
|
|||||||
}
|
}
|
||||||
return ehRequest(url, url, EhUrl.origin) {
|
return ehRequest(url, url, EhUrl.origin) {
|
||||||
formBody {
|
formBody {
|
||||||
add("ddact", catStr)
|
append("ddact", catStr)
|
||||||
gidArray.forEach { add("modifygids[]", it.toString()) }
|
gidArray.forEach { append("modifygids[]", it.toString()) }
|
||||||
}
|
}
|
||||||
}.executeAndParsingWith(FavoritesParser::parse).apply { fillGalleryList(galleryInfoList, url) }
|
}.fetchUsingAsByteBuffer(FavoritesParser::parse).apply { galleryInfoList.fillInfo(url) }
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getGalleryPageApi(
|
suspend fun getGalleryPageApi(gid: Long, index: Int, pToken: String, showKey: String?, previousPToken: String?): GalleryPageParser.Result {
|
||||||
gid: Long,
|
|
||||||
index: Int,
|
|
||||||
pToken: String,
|
|
||||||
showKey: String?,
|
|
||||||
previousPToken: String?,
|
|
||||||
): GalleryPageApiParser.Result {
|
|
||||||
val referer = if (index > 0 && previousPToken != null) EhUrl.getPageUrl(gid, index - 1, previousPToken) else null
|
val referer = if (index > 0 && previousPToken != null) EhUrl.getPageUrl(gid, index - 1, previousPToken) else null
|
||||||
return ehRequest(EhUrl.apiUrl, referer, EhUrl.origin) {
|
return ehRequest(EhUrl.apiUrl, referer, EhUrl.origin) {
|
||||||
jsonBody {
|
jsonBody {
|
||||||
@@ -376,82 +360,70 @@ object EhEngine {
|
|||||||
put("imgkey", pToken)
|
put("imgkey", pToken)
|
||||||
put("showkey", showKey)
|
put("showkey", showKey)
|
||||||
}
|
}
|
||||||
}.executeAndParsingWith(GalleryPageApiParser::parse)
|
}.fetchUsingAsText { GalleryPageParser.parse(filterNot { it == '\\' }) }
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun rateGallery(
|
suspend fun rateGallery(apiUid: Long, apiKey: String?, gid: Long, token: String, rating: Float): RateGalleryResult = ehRequest(EhUrl.apiUrl, EhUrl.getGalleryDetailUrl(gid, token), EhUrl.origin) {
|
||||||
apiUid: Long,
|
|
||||||
apiKey: String?,
|
|
||||||
gid: Long,
|
|
||||||
token: String?,
|
|
||||||
rating: Float,
|
|
||||||
): RateGalleryResult = ehRequest(EhUrl.apiUrl, EhUrl.getGalleryDetailUrl(gid, token), EhUrl.origin) {
|
|
||||||
jsonBody {
|
jsonBody {
|
||||||
put("method", "rategallery")
|
put("method", "rategallery")
|
||||||
put("apiuid", apiUid)
|
put("apiuid", apiUid)
|
||||||
put("apikey", apiKey)
|
put("apikey", requireNotNull(apiKey))
|
||||||
put("gid", gid)
|
put("gid", gid)
|
||||||
put("token", token)
|
put("token", token)
|
||||||
put("rating", ceil((rating * 2).toDouble()).toInt())
|
put("rating", ceil((rating * 2).toDouble()).toInt())
|
||||||
}
|
}
|
||||||
}.executeAndParsingWith(String::parseAs)
|
}.fetchUsingAsText(String::parseAs)
|
||||||
|
|
||||||
suspend fun fillGalleryListByApi(galleryInfoList: List<GalleryInfo>, referer: String) = galleryInfoList.chunked(MAX_REQUEST_SIZE).parMap {
|
suspend fun fillGalleryListByApi(galleryInfoList: List<GalleryInfo>, referer: String? = null) =
|
||||||
ehRequest(EhUrl.apiUrl, referer, EhUrl.origin) {
|
galleryInfoList.chunked(MAX_REQUEST_SIZE).chunked(MAX_SEQUENTIAL_REQUESTS).forEachIndexed { index, chunk ->
|
||||||
jsonBody {
|
if (index != 0) {
|
||||||
put("method", "gdata")
|
delay(REQUEST_INTERVAL)
|
||||||
array("gidlist") {
|
}
|
||||||
it.forEach {
|
chunk.parMap {
|
||||||
addJsonArray {
|
ehRequest(EhUrl.apiUrl, referer, EhUrl.origin) {
|
||||||
add(it.gid)
|
jsonBody {
|
||||||
add(it.token)
|
put("method", "gdata")
|
||||||
|
array("gidlist") {
|
||||||
|
it.forEach {
|
||||||
|
addJsonArray {
|
||||||
|
add(it.gid)
|
||||||
|
add(it.token)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
put("namespace", 1)
|
||||||
}
|
}
|
||||||
}
|
}.fetchUsingAsText { GalleryApiParser.parse(this, it) }
|
||||||
put("namespace", 1)
|
|
||||||
}
|
}
|
||||||
}.executeAndParsingWith { GalleryApiParser.parse(this, it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
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("gid", gid)
|
|
||||||
put("token", token)
|
|
||||||
put("comment_id", commentId)
|
|
||||||
put("comment_vote", commentVote)
|
|
||||||
}
|
}
|
||||||
}.executeAndParsingWith(String::parseAs)
|
|
||||||
|
|
||||||
suspend fun voteTag(
|
suspend fun voteComment(apiUid: Long, apiKey: String?, gid: Long, token: String, commentId: Long, commentVote: Int): VoteCommentResult =
|
||||||
apiUid: Long,
|
ehRequest(EhUrl.apiUrl, EhUrl.referer, EhUrl.origin) {
|
||||||
apiKey: String?,
|
jsonBody {
|
||||||
gid: Long,
|
put("method", "votecomment")
|
||||||
token: String?,
|
put("apiuid", apiUid)
|
||||||
tags: String?,
|
put("apikey", requireNotNull(apiKey))
|
||||||
vote: Int,
|
put("gid", gid)
|
||||||
) = ehRequest(EhUrl.apiUrl, EhUrl.referer, EhUrl.origin) {
|
put("token", token)
|
||||||
jsonBody {
|
put("comment_id", commentId)
|
||||||
put("method", "taggallery")
|
put("comment_vote", commentVote)
|
||||||
put("apiuid", apiUid)
|
}
|
||||||
put("apikey", apiKey)
|
}.fetchUsingAsText(String::parseAs)
|
||||||
put("gid", gid)
|
|
||||||
put("token", token)
|
|
||||||
put("tags", tags)
|
|
||||||
put("vote", vote)
|
|
||||||
}
|
|
||||||
}.executeAndParsingWith(VoteTagParser::parse)
|
|
||||||
|
|
||||||
suspend fun getGalleryToken(gid: Long, gtoken: String?, page: Int) = ehRequest(EhUrl.apiUrl, EhUrl.referer, EhUrl.origin) {
|
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", requireNotNull(apiKey))
|
||||||
|
put("gid", gid)
|
||||||
|
put("token", token)
|
||||||
|
put("tags", tags)
|
||||||
|
put("vote", vote)
|
||||||
|
}
|
||||||
|
}.fetchUsingAsText(VoteTagParser::parse)
|
||||||
|
|
||||||
|
suspend fun getGalleryToken(gid: Long, gtoken: String, page: Int) = ehRequest(EhUrl.apiUrl, EhUrl.referer, EhUrl.origin) {
|
||||||
jsonBody {
|
jsonBody {
|
||||||
put("method", "gtoken")
|
put("method", "gtoken")
|
||||||
array("pagelist") {
|
array("pagelist") {
|
||||||
@@ -462,47 +434,32 @@ object EhEngine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.executeAndParsingWith(GalleryTokenApiParser::parse)
|
}.fetchUsingAsText(GalleryTokenApiParser::parse)
|
||||||
|
|
||||||
/**
|
private suspend fun MutableList<BaseGalleryInfo>.fillInfo(url: String, filter: Boolean = false) = with(EhFilter) {
|
||||||
* @param image Must be jpeg
|
if (filter) removeAllSuspend { filterTitle(it) || filterUploader(it) }
|
||||||
*/
|
val hasTags = any { !it.simpleTags.isNullOrEmpty() }
|
||||||
suspend fun imageSearch(image: File, uss: Boolean, osc: Boolean) = ehRequest(EhUrl.imageSearchUrl, EhUrl.referer, EhUrl.origin) {
|
val hasPages = any { it.pages != 0 }
|
||||||
multipartBody {
|
val hasRated = any { it.rated }
|
||||||
setType(MultipartBody.FORM)
|
val needApi = filter && needTags() && !hasTags || Settings.showGalleryPages.value && !hasPages || hasRated
|
||||||
addFormDataPart("sfile", "a.jpg", image.asRequestBody(MEDIA_TYPE_JPEG))
|
if (needApi) fillGalleryListByApi(this@fillInfo, url)
|
||||||
if (uss) addFormDataPart("fs_similar", "on")
|
if (filter) removeAllSuspend { filterUploader(it) || filterTag(it) || filterTagNamespace(it) }
|
||||||
if (osc) addFormDataPart("fs_covers", "on")
|
forEach {
|
||||||
addFormDataPart("f_sfile", "File Search")
|
if (it.favoriteSlot == GalleryInfo.NOT_FAVORITED && EhDB.containLocalFavorites(it.gid)) {
|
||||||
|
it.favoriteSlot = GalleryInfo.LOCAL_FAVORITED
|
||||||
|
}
|
||||||
|
if (!needApi) it.generateSLang()
|
||||||
}
|
}
|
||||||
}.executeAndParsingWith(GalleryListParser::parse).apply { fillGalleryList(galleryInfoList, EhUrl.imageSearchUrl) }
|
|
||||||
|
|
||||||
private suspend fun fillGalleryList(list: MutableList<BaseGalleryInfo>, url: String, filter: Boolean = false) {
|
|
||||||
// Filter title and uploader
|
|
||||||
if (filter) list.removeAllSuspend { EhFilter.filterTitle(it) || EhFilter.filterUploader(it) }
|
|
||||||
|
|
||||||
var hasTags = false
|
|
||||||
var hasPages = false
|
|
||||||
var hasRated = false
|
|
||||||
for (gi in list) {
|
|
||||||
if (!gi.simpleTags.isNullOrEmpty()) {
|
|
||||||
hasTags = true
|
|
||||||
}
|
|
||||||
if (gi.pages != 0) {
|
|
||||||
hasPages = true
|
|
||||||
}
|
|
||||||
if (gi.rated) {
|
|
||||||
hasRated = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val needApi = filter && EhFilter.needTags() && !hasTags || Settings.showGalleryPages && !hasPages || hasRated
|
|
||||||
if (needApi) fillGalleryListByApi(list, url)
|
|
||||||
|
|
||||||
// Filter tag, thumbnail mode need filter uploader again
|
|
||||||
if (filter) list.removeAllSuspend { EhFilter.filterUploader(it) || EhFilter.filterTag(it) || EhFilter.filterTagNamespace(it) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun modifyFavoritesRange(galleryList: List<Pair<Long, String>>, dstCat: Int) {
|
suspend fun addFavorites(galleryList: List<Pair<Long, String>>, dstCat: Int) {
|
||||||
galleryList.forEach { (gid, token) -> modifyFavorites(gid, token, dstCat) }
|
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,115 +15,94 @@
|
|||||||
*/
|
*/
|
||||||
package com.hippo.ehviewer.client
|
package com.hippo.ehviewer.client
|
||||||
|
|
||||||
import android.util.Log
|
import com.hippo.ehviewer.BuildConfig
|
||||||
import com.hippo.ehviewer.EhApplication.Companion.baseOkHttpClient
|
import com.hippo.ehviewer.EhApplication.Companion.ktorClient
|
||||||
import com.hippo.ehviewer.EhApplication.Companion.noRedirectOkHttpClient
|
import com.hippo.ehviewer.EhApplication.Companion.noRedirectKtorClient
|
||||||
import com.hippo.ehviewer.EhApplication.Companion.okHttpClient
|
|
||||||
import com.hippo.ehviewer.Settings
|
import com.hippo.ehviewer.Settings
|
||||||
import kotlin.contracts.InvocationKind
|
import com.hippo.ehviewer.util.bodyAsUtf8Text
|
||||||
import kotlin.contracts.contract
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
import kotlin.coroutines.resume
|
import io.ktor.client.request.HttpRequestBuilder
|
||||||
import kotlin.coroutines.resumeWithException
|
import io.ktor.client.request.forms.FormDataContent
|
||||||
import kotlinx.coroutines.coroutineScope
|
import io.ktor.client.request.header
|
||||||
import kotlinx.coroutines.launch
|
import io.ktor.client.request.prepareRequest
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
import io.ktor.client.request.setBody
|
||||||
|
import io.ktor.client.statement.HttpResponse
|
||||||
|
import io.ktor.client.statement.HttpStatement
|
||||||
|
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.Json
|
||||||
import kotlinx.serialization.json.JsonArrayBuilder
|
import kotlinx.serialization.json.JsonArrayBuilder
|
||||||
import kotlinx.serialization.json.JsonObjectBuilder
|
import kotlinx.serialization.json.JsonObjectBuilder
|
||||||
import kotlinx.serialization.json.buildJsonArray
|
import kotlinx.serialization.json.buildJsonArray
|
||||||
import kotlinx.serialization.json.buildJsonObject
|
import kotlinx.serialization.json.buildJsonObject
|
||||||
import kotlinx.serialization.json.okio.decodeFromBufferedSource
|
import kotlinx.serialization.json.okio.decodeFromBufferedSource
|
||||||
import okhttp3.Call
|
|
||||||
import okhttp3.FormBody
|
|
||||||
import okhttp3.MediaType
|
|
||||||
import okhttp3.MediaType.Companion.toMediaType
|
|
||||||
import okhttp3.MultipartBody
|
|
||||||
import okhttp3.Request
|
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
|
||||||
import okhttp3.Response
|
|
||||||
import okhttp3.executeAsync
|
|
||||||
import okio.BufferedSource
|
import okio.BufferedSource
|
||||||
|
|
||||||
inline fun ehRequest(url: String, referer: String? = null, origin: String? = null, builder: Request.Builder.() -> Unit = {}) = Request.Builder().url(url).apply {
|
|
||||||
addHeader("User-Agent", Settings.userAgent)
|
|
||||||
addHeader("Accept", CHROME_ACCEPT)
|
|
||||||
addHeader("Accept-Language", CHROME_ACCEPT_LANGUAGE)
|
|
||||||
referer?.let { addHeader("Referer", it) }
|
|
||||||
origin?.let { addHeader("Origin", it) }
|
|
||||||
}.apply(builder).build()
|
|
||||||
|
|
||||||
inline fun Request.Builder.formBody(builder: FormBody.Builder.() -> Unit) = post(FormBody.Builder().apply(builder).build())
|
|
||||||
|
|
||||||
inline fun Request.Builder.multipartBody(builder: MultipartBody.Builder.() -> Unit) = post(MultipartBody.Builder().apply(builder).build())
|
|
||||||
|
|
||||||
val MEDIA_TYPE_JSON: MediaType = "application/json; charset=utf-8".toMediaType()
|
|
||||||
|
|
||||||
inline fun JsonObjectBuilder.array(name: String, builder: JsonArrayBuilder.() -> Unit) = put(name, buildJsonArray(builder))
|
inline fun JsonObjectBuilder.array(name: String, builder: JsonArrayBuilder.() -> Unit) = put(name, buildJsonArray(builder))
|
||||||
|
|
||||||
inline fun Request.Builder.jsonBody(builder: JsonObjectBuilder.() -> Unit) = post(buildJsonObject(builder).toString().toRequestBody(MEDIA_TYPE_JSON))
|
suspend inline fun <reified T> HttpStatement.executeAndParseAs() = executeSafely { it.bodyAsUtf8Text().parseAs<T>() }
|
||||||
|
|
||||||
const val TAG_CALL = "CancellableCallReadingScope"
|
|
||||||
|
|
||||||
// This scoped response reading scope suspend function aimed at cancelling the call immediately once coroutine is cancelled.
|
|
||||||
// i.e., bind a call with a suspended coroutine, and launch another coroutine to execute reading
|
|
||||||
// Once origin coroutine is cancelled, then call cancelled, socket is closed, the reading coroutine will encounter [IOException]
|
|
||||||
// Since Okio is not suspendable/cancellable, also okhttp, [executeAsync] only make [Request -> Response] cancellable, reading a Response is still not cancellable
|
|
||||||
// Considering interrupting is not safe and performant, See https://github.com/Kotlin/kotlinx.coroutines/issues/3551#issuecomment-1353245978, we have such [usingCancellable]
|
|
||||||
// TODO: Remove logging after this function stable
|
|
||||||
suspend inline fun <R> Call.usingCancellable(crossinline block: suspend Response.() -> R): R = executeAsync().use {
|
|
||||||
val call = this
|
|
||||||
Log.d(TAG_CALL, "Got response of call$call")
|
|
||||||
coroutineScope {
|
|
||||||
suspendCancellableCoroutine<R> { cont ->
|
|
||||||
launch {
|
|
||||||
val r = runCatching {
|
|
||||||
Log.d(TAG_CALL, "Reading response of call$call")
|
|
||||||
block(it)
|
|
||||||
}
|
|
||||||
if (!isCanceled() && cont.isActive) {
|
|
||||||
r.exceptionOrNull()?.let {
|
|
||||||
Log.e(TAG_CALL, "Reading response of call$call failed!")
|
|
||||||
cont.resumeWithException(it)
|
|
||||||
}
|
|
||||||
r.getOrNull()?.let {
|
|
||||||
Log.d(TAG_CALL, "Reading response of call$call succeed!")
|
|
||||||
cont.resume(it)
|
|
||||||
}
|
|
||||||
} else if (isCanceled() && cont.isActive) {
|
|
||||||
Log.e(TAG_CALL, "call$call cancelled but coroutine is Active!")
|
|
||||||
} else if (!isCanceled() && !cont.isActive) {
|
|
||||||
Log.e(TAG_CALL, "call$call not cancelled but coroutine is Dead!")
|
|
||||||
} else {
|
|
||||||
Log.d(TAG_CALL, "Reading response of call$call cancelled")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cont.invokeOnCancellation {
|
|
||||||
Log.d(TAG_CALL, "Cancelling call$call")
|
|
||||||
cancel()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.apply {
|
|
||||||
Log.d(TAG_CALL, "Reading response of call$call finished!")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend inline fun <R> Request.execute(block: Response.() -> R): R {
|
|
||||||
contract {
|
|
||||||
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
|
|
||||||
}
|
|
||||||
return okHttpClient.newCall(this).executeAsync().use(block)
|
|
||||||
}
|
|
||||||
suspend inline fun <R> Request.executeNonCache(crossinline block: suspend Response.() -> R) = baseOkHttpClient.newCall(this).usingCancellable(block)
|
|
||||||
suspend inline fun <R> Request.executeNoRedirect(block: Response.() -> R) = noRedirectOkHttpClient.newCall(this).executeAsync().use(block)
|
|
||||||
|
|
||||||
suspend inline fun <reified T> Request.executeAndParseAs() = execute { parseAs<T>() }
|
|
||||||
|
|
||||||
inline fun <reified T> Response.parseAs(): T = body.source().parseAs()
|
|
||||||
inline fun <reified T> BufferedSource.parseAs(): T = json.decodeFromBufferedSource(this)
|
inline fun <reified T> BufferedSource.parseAs(): T = json.decodeFromBufferedSource(this)
|
||||||
inline fun <reified T> String.parseAs(): T = json.decodeFromString(this)
|
inline fun <reified T> String.parseAs(): T = json.decodeFromString(this)
|
||||||
|
|
||||||
val json = Json { ignoreUnknownKeys = true }
|
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 = "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"
|
const val CHROME_ACCEPT_LANGUAGE = "en-US,en;q=0.5"
|
||||||
|
|
||||||
|
fun HttpRequestBuilder.applyEhConfig(referer: String?, origin: String?) {
|
||||||
|
method = HttpMethod.Get
|
||||||
|
header("Referer", referer)
|
||||||
|
header("Origin", origin)
|
||||||
|
userAgent(Settings.userAgent)
|
||||||
|
header(HttpHeaders.Accept, CHROME_ACCEPT)
|
||||||
|
header(HttpHeaders.AcceptLanguage, CHROME_ACCEPT_LANGUAGE)
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
referer: String? = null,
|
||||||
|
origin: String? = null,
|
||||||
|
builder: HttpRequestBuilder.() -> Unit = {},
|
||||||
|
) = 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) {
|
||||||
|
method = HttpMethod.Post
|
||||||
|
val parameters = ParametersBuilder().apply(builder).build()
|
||||||
|
setBody(FormDataContent(parameters))
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun HttpRequestBuilder.jsonBody(builder: JsonObjectBuilder.() -> Unit) {
|
||||||
|
method = HttpMethod.Post
|
||||||
|
val json = buildJsonObject(builder).toString()
|
||||||
|
setBody(TextContent(text = json, contentType = ContentType.Application.Json))
|
||||||
|
}
|
||||||
@@ -19,11 +19,20 @@ import android.content.Context
|
|||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import com.hippo.ehviewer.EhApplication.Companion.baseOkHttpClient
|
import com.hippo.ehviewer.EhApplication.Companion.ktorClient
|
||||||
import com.hippo.ehviewer.R
|
import com.hippo.ehviewer.R
|
||||||
|
import com.hippo.ehviewer.client.data.TagNamespace
|
||||||
import com.hippo.ehviewer.util.AppConfig
|
import com.hippo.ehviewer.util.AppConfig
|
||||||
import com.hippo.ehviewer.util.FileUtils
|
import com.hippo.ehviewer.util.FileUtils
|
||||||
import com.hippo.ehviewer.util.copyToFile
|
import com.hippo.ehviewer.util.StatusCodeException
|
||||||
|
import com.hippo.ehviewer.util.copyTo
|
||||||
|
import com.hippo.unifile.asUniFile
|
||||||
|
import com.hippo.unifile.sha1
|
||||||
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
|
import io.ktor.client.HttpClient
|
||||||
|
import io.ktor.client.request.prepareGet
|
||||||
|
import io.ktor.client.statement.bodyAsChannel
|
||||||
|
import io.ktor.http.isSuccess
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.nio.charset.StandardCharsets
|
import java.nio.charset.StandardCharsets
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
@@ -35,12 +44,7 @@ import kotlinx.coroutines.launch
|
|||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import moe.tarsin.coroutines.runSuspendCatching
|
import moe.tarsin.coroutines.runSuspendCatching
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import okhttp3.Request
|
|
||||||
import okhttp3.executeAsync
|
|
||||||
import okio.BufferedSource
|
import okio.BufferedSource
|
||||||
import okio.HashingSink.Companion.sha1
|
|
||||||
import okio.blackholeSink
|
|
||||||
import okio.buffer
|
import okio.buffer
|
||||||
import okio.source
|
import okio.source
|
||||||
import splitties.init.appCtx
|
import splitties.init.appCtx
|
||||||
@@ -60,7 +64,7 @@ object EhTagDatabase : CoroutineScope {
|
|||||||
var initialized by mutableStateOf(false)
|
var initialized by mutableStateOf(false)
|
||||||
|
|
||||||
fun getTranslation(prefix: String? = NAMESPACE_PREFIX, tag: String?): String? {
|
fun getTranslation(prefix: String? = NAMESPACE_PREFIX, tag: String?): String? {
|
||||||
return tagGroups[prefix]?.get(tag)?.trim()?.takeIf { it.isNotEmpty() }
|
return tagGroups[prefix]?.get(tag)?.trim()?.ifEmpty { null }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun internalSuggestFlow(
|
private fun internalSuggestFlow(
|
||||||
@@ -103,7 +107,7 @@ object EhTagDatabase : CoroutineScope {
|
|||||||
val prefix = keyword.dropLast(mKeyword.length)
|
val prefix = keyword.dropLast(mKeyword.length)
|
||||||
val namespace = mKeyword.substringBefore(':')
|
val namespace = mKeyword.substringBefore(':')
|
||||||
val tagKeyword = mKeyword.drop(namespace.length + 1)
|
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 }]
|
val tags = tagGroups[namespacePrefix.takeIf { tagKeyword.isNotEmpty() && it != NAMESPACE_PREFIX }]
|
||||||
tags?.let {
|
tags?.let {
|
||||||
internalSuggestFlow(it, tagKeyword, translate, exactly).collect { (hint, tag) ->
|
internalSuggestFlow(it, tagKeyword, translate, exactly).collect { (hint, tag) ->
|
||||||
@@ -127,24 +131,6 @@ object EhTagDatabase : CoroutineScope {
|
|||||||
private fun String.containsIgnoreSpace(other: String, ignoreCase: Boolean = true): Boolean =
|
private fun String.containsIgnoreSpace(other: String, ignoreCase: Boolean = true): Boolean =
|
||||||
removeSpace().contains(other.removeSpace(), ignoreCase)
|
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"
|
|
||||||
}
|
|
||||||
|
|
||||||
fun namespaceToPrefix(namespace: String): String? {
|
|
||||||
return NAMESPACE_TO_PREFIX[namespace]
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getMetadata(context: Context): Array<String>? {
|
private fun getMetadata(context: Context): Array<String>? {
|
||||||
return context.resources.getStringArray(R.array.tag_translation_metadata)
|
return context.resources.getStringArray(R.array.tag_translation_metadata)
|
||||||
.takeIf { it.size == 4 }
|
.takeIf { it.size == 4 }
|
||||||
@@ -160,39 +146,22 @@ object EhTagDatabase : CoroutineScope {
|
|||||||
}.getOrNull()
|
}.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 {
|
private fun checkData(sha1: String?, data: File): Boolean {
|
||||||
return sha1 != null && sha1 == getFileSha1(data)
|
return sha1 != null && sha1 == data.asUniFile().sha1()
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun save(client: OkHttpClient, url: String, file: File): Boolean {
|
private suspend fun save(client: HttpClient, url: String, file: File) {
|
||||||
val request: Request = Request.Builder().url(url).build()
|
|
||||||
val call = client.newCall(request)
|
|
||||||
runCatching {
|
runCatching {
|
||||||
call.executeAsync().use { response ->
|
client.prepareGet(url).executeSafely {
|
||||||
if (!response.isSuccessful) {
|
if (it.status.isSuccess()) {
|
||||||
return false
|
it.bodyAsChannel().copyTo(file)
|
||||||
|
} else {
|
||||||
|
throw StatusCodeException(it.status.value)
|
||||||
}
|
}
|
||||||
response.body.use {
|
|
||||||
it.copyToFile(file)
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
file.delete()
|
file.delete()
|
||||||
it.printStackTrace()
|
}.getOrThrow()
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun update() {
|
suspend fun update() {
|
||||||
@@ -217,7 +186,7 @@ object EhTagDatabase : CoroutineScope {
|
|||||||
runSuspendCatching {
|
runSuspendCatching {
|
||||||
issueUpdateInMemoryData()
|
issueUpdateInMemoryData()
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
it.printStackTrace()
|
logcat(it)
|
||||||
}
|
}
|
||||||
updateInternal()
|
updateInternal()
|
||||||
}
|
}
|
||||||
@@ -246,7 +215,7 @@ object EhTagDatabase : CoroutineScope {
|
|||||||
|
|
||||||
// Save new sha1
|
// Save new sha1
|
||||||
val tempSha1File = File(dir, "$sha1Name.tmp")
|
val tempSha1File = File(dir, "$sha1Name.tmp")
|
||||||
check(save(baseOkHttpClient, sha1Url, tempSha1File))
|
save(ktorClient, sha1Url, tempSha1File)
|
||||||
val tempSha1 = getFileContent(tempSha1File)
|
val tempSha1 = getFileContent(tempSha1File)
|
||||||
|
|
||||||
// Check new sha1 and current sha1
|
// Check new sha1 and current sha1
|
||||||
@@ -258,7 +227,7 @@ object EhTagDatabase : CoroutineScope {
|
|||||||
|
|
||||||
// Save new data
|
// Save new data
|
||||||
val tempDataFile = File(dir, "$dataName.tmp")
|
val tempDataFile = File(dir, "$dataName.tmp")
|
||||||
check(save(baseOkHttpClient, dataUrl, tempDataFile))
|
save(ktorClient, dataUrl, tempDataFile)
|
||||||
|
|
||||||
// Check new sha1 and new data
|
// Check new sha1 and new data
|
||||||
if (!checkData(tempSha1, tempDataFile)) {
|
if (!checkData(tempSha1, tempDataFile)) {
|
||||||
@@ -276,7 +245,7 @@ object EhTagDatabase : CoroutineScope {
|
|||||||
// Read new EhTagDatabase
|
// Read new EhTagDatabase
|
||||||
issueUpdateInMemoryData(dataFile)
|
issueUpdateInMemoryData(dataFile)
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
it.printStackTrace()
|
logcat(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,10 @@
|
|||||||
package com.hippo.ehviewer.client
|
package com.hippo.ehviewer.client
|
||||||
|
|
||||||
import com.hippo.ehviewer.Settings
|
import com.hippo.ehviewer.Settings
|
||||||
import okhttp3.HttpUrl
|
import io.ktor.http.Parameters
|
||||||
|
import io.ktor.http.ParametersBuilder
|
||||||
|
import io.ktor.http.URLBuilder
|
||||||
|
import io.ktor.http.URLProtocol
|
||||||
|
|
||||||
object EhUrl {
|
object EhUrl {
|
||||||
const val SITE_E = 0
|
const val SITE_E = 0
|
||||||
@@ -49,18 +52,11 @@ object EhUrl {
|
|||||||
const val REFERER_E = "https://$DOMAIN_E"
|
const val REFERER_E = "https://$DOMAIN_E"
|
||||||
const val ORIGIN_E = REFERER_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
|
val domain: String
|
||||||
get() = when (Settings.gallerySite) {
|
get() = when (Settings.gallerySite) {
|
||||||
SITE_E -> DOMAIN_E
|
SITE_E -> DOMAIN_E
|
||||||
SITE_EX -> DOMAIN_EX
|
SITE_EX -> DOMAIN_EX
|
||||||
else -> HOST_E
|
else -> DOMAIN_E
|
||||||
}
|
}
|
||||||
|
|
||||||
val apiUrl: String
|
val apiUrl: String
|
||||||
@@ -116,39 +112,54 @@ object EhUrl {
|
|||||||
else -> URL_IMAGE_SEARCH_E
|
else -> URL_IMAGE_SEARCH_E
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getGalleryDetailUrl(gid: Long, token: String?): String {
|
fun getGalleryDetailUrl(gid: Long, token: String, index: Int = 0, allComment: Boolean = false) =
|
||||||
return getGalleryDetailUrl(gid, token, 0, 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 {
|
fun getGalleryMultiPageViewerUrl(gid: Long, token: String) = ehUrl(listOf("mpv", "$gid", token)).buildString()
|
||||||
addPathSegments("g/$gid/$token/")
|
|
||||||
if (index != 0) addQueryParameter("p", "$index")
|
|
||||||
if (allComment) addQueryParameter("hc", "1")
|
|
||||||
}.toString()
|
|
||||||
|
|
||||||
fun getGalleryMultiPageViewerUrl(gid: Long, token: String) = ehUrl {
|
fun getPageUrl(gid: Long, index: Int, pToken: String, nl: String? = null) = ehUrl(listOf("s", pToken, "$gid-${index + 1}")) {
|
||||||
addPathSegments("mpv/$gid/$token/")
|
addQueryParameterIfNotBlank("nl", nl)
|
||||||
}.toString()
|
}.buildString()
|
||||||
|
|
||||||
fun getPageUrl(gid: Long, index: Int, pToken: String): String {
|
fun getAddFavorites(gid: Long, token: String) = ehUrl("gallerypopups.php") {
|
||||||
return host + "s/" + pToken + '/' + gid + '-' + (index + 1)
|
addQueryParameter("gid", "$gid")
|
||||||
}
|
addQueryParameter("t", token)
|
||||||
|
addQueryParameter("act", "addfav")
|
||||||
|
}.buildString()
|
||||||
|
|
||||||
fun getAddFavorites(gid: Long, token: String?): String {
|
fun getDownloadArchive(gid: Long, token: String, or: String) = ehUrl("archiver.php") {
|
||||||
return host + "gallerypopups.php?gid=" + gid + "&t=" + token + "&act=addfav"
|
addQueryParameter("gid", "$gid")
|
||||||
}
|
addQueryParameter("token", token)
|
||||||
|
addQueryParameter("or", or)
|
||||||
|
}.buildString()
|
||||||
|
|
||||||
fun getDownloadArchive(gid: Long, token: String?, or: String): String {
|
fun getTagDefinitionUrl(tag: String) = "https://ehwiki.org/wiki/" + tag.replace(' ', '_')
|
||||||
return host + "archiver.php?gid=" + gid + "&token=" + token + "&or=" + or
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getTagDefinitionUrl(tag: String): String {
|
|
||||||
return "https://ehwiki.org/wiki/" + tag.replace(' ', '_')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
inline fun httpsUrl(builder: HttpUrl.Builder.() -> Unit) = HttpUrl.Builder().apply(builder).scheme("https").build()
|
inline fun ehUrl(path: String? = null, host: String = EhUrl.domain, builder: ParametersBuilder.() -> Unit) = ehUrl(
|
||||||
inline fun ehUrl(builder: HttpUrl.Builder.() -> Unit) = httpsUrl { apply(builder).host(EhUrl.domain) }
|
path = if (path.isNullOrBlank()) emptyList() else listOf(path),
|
||||||
fun HttpUrl.Builder.addQueryParameterIfNotBlank(name: String, value: String?) = apply {
|
host = host,
|
||||||
if (!value.isNullOrBlank()) addQueryParameter(name, value)
|
parameters = Parameters.build(builder),
|
||||||
|
)
|
||||||
|
|
||||||
|
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>, host: String = EhUrl.domain, parameters: Parameters = Parameters.Empty) = URLBuilder(
|
||||||
|
protocol = URLProtocol.HTTPS,
|
||||||
|
host = host,
|
||||||
|
pathSegments = path,
|
||||||
|
parameters = parameters,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun ParametersBuilder.addQueryParameterIfNotBlank(name: String, value: String?) = apply {
|
||||||
|
if (!value.isNullOrBlank()) append(name, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun ParametersBuilder.addQueryParameter(name: String, value: String) = append(name, value)
|
||||||
@@ -15,18 +15,17 @@
|
|||||||
*/
|
*/
|
||||||
package com.hippo.ehviewer.client
|
package com.hippo.ehviewer.client
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import androidx.compose.material3.MaterialTheme
|
||||||
import android.content.Context
|
|
||||||
import androidx.compose.runtime.Composable
|
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.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.graphics.toArgb
|
||||||
import com.google.android.material.R
|
import androidx.core.graphics.ColorUtils
|
||||||
import com.google.android.material.color.MaterialColors
|
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.Settings
|
||||||
import com.hippo.ehviewer.client.data.GalleryInfo
|
import com.hippo.ehviewer.client.data.GalleryInfo
|
||||||
import java.util.regex.Pattern
|
import kotlin.math.abs
|
||||||
|
|
||||||
object EhUtils {
|
object EhUtils {
|
||||||
const val NONE = -1 // Use it for homepage
|
const val NONE = -1 // Use it for homepage
|
||||||
@@ -58,14 +57,14 @@ object EhUtils {
|
|||||||
private const val BG_COLOR_UNKNOWN = 0xff000000u
|
private const val BG_COLOR_UNKNOWN = 0xff000000u
|
||||||
|
|
||||||
// Remove [XXX], (XXX), {XXX}, ~XXX~ stuff
|
// Remove [XXX], (XXX), {XXX}, ~XXX~ stuff
|
||||||
private val PATTERN_TITLE_PREFIX = Pattern.compile(
|
private val PATTERN_TITLE_PREFIX = Regex(
|
||||||
"^(?:\\([^)]*\\)|\\[[^]]*]|\\{[^}]*\\}|~[^~]*~|\\s+)*",
|
"""^(?:\([^)]*\)|\[[^]]*]|\{[^}]*\}|~[^~]*~|\s+)*""",
|
||||||
)
|
)
|
||||||
|
|
||||||
// Remove [XXX], (XXX), {XXX}, ~XXX~ stuff and something like ch. 1-23
|
// Remove [XXX], (XXX), {XXX}, ~XXX~ stuff and something like ch. 1-23
|
||||||
private val PATTERN_TITLE_SUFFIX = Pattern.compile(
|
private val PATTERN_TITLE_SUFFIX = Regex(
|
||||||
"(?:\\s+ch.[\\s\\d-]+)?(?:\\([^)]*\\)|\\[[^]]*]|\\{[^}]*\\}|~[^~]*~|\\s+)*$",
|
"""(?:\s+ch.[\s\d-]+)?(?:\([^)]*\)|\[[^]]*]|\{[^}]*\}|~[^~]*~|\s+)*$""",
|
||||||
Pattern.CASE_INSENSITIVE,
|
RegexOption.IGNORE_CASE,
|
||||||
)
|
)
|
||||||
|
|
||||||
private val CATEGORY_VALUES = hashMapOf(
|
private val CATEGORY_VALUES = hashMapOf(
|
||||||
@@ -101,18 +100,33 @@ object EhUtils {
|
|||||||
return CATEGORY_VALUES.getOrDefault(type, CATEGORY_VALUES[UNKNOWN])!![0]
|
return CATEGORY_VALUES.getOrDefault(type, CATEGORY_VALUES[UNKNOWN])!![0]
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("RestrictedApi")
|
private fun differenceDegrees(a: Float, b: Float): Float {
|
||||||
fun harmonizeWithRole(context: Context, src: Int): Int {
|
return 180.0f - abs(abs(a - b) - 180.0f)
|
||||||
val primary = MaterialColors.getColor(context, R.attr.colorPrimaryContainer, "EhUtils")
|
|
||||||
val fromHct = Hct.fromInt(src)
|
|
||||||
val toHct = Hct.fromInt(primary)
|
|
||||||
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))
|
|
||||||
return Hct.from(outputHue, toHct.chroma, toHct.tone).toInt()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getCategoryColor(context: Context, category: Int): Int {
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
val harmonizeWithRole = { primaryContainer: Int, src: Int ->
|
||||||
|
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
|
||||||
|
@ReadOnlyComposable
|
||||||
|
@Composable
|
||||||
|
fun getCategoryColor(category: Int): Color {
|
||||||
val primary = when (category) {
|
val primary = when (category) {
|
||||||
DOUJINSHI -> BG_COLOR_DOUJINSHI
|
DOUJINSHI -> BG_COLOR_DOUJINSHI
|
||||||
MANGA -> BG_COLOR_MANGA
|
MANGA -> BG_COLOR_MANGA
|
||||||
@@ -126,22 +140,20 @@ object EhUtils {
|
|||||||
MISC -> BG_COLOR_MISC
|
MISC -> BG_COLOR_MISC
|
||||||
else -> BG_COLOR_UNKNOWN
|
else -> BG_COLOR_UNKNOWN
|
||||||
}.toInt()
|
}.toInt()
|
||||||
return if (Settings.harmonizeCategoryColor) {
|
val color = if (Settings.harmonizeCategoryColor) {
|
||||||
harmonizeWithRole(context, primary)
|
val primaryContainer = MaterialTheme.colorScheme.primaryContainer.toArgb()
|
||||||
|
harmonizeWithRole(primaryContainer, primary)
|
||||||
} else {
|
} else {
|
||||||
primary
|
primary
|
||||||
}
|
}
|
||||||
|
return Color(color)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun getCategoryColor(category: Int) = Color(getCategoryColor(LocalContext.current, category))
|
|
||||||
|
|
||||||
val categoryTextColor = Color(0xffe6e0e9)
|
val categoryTextColor = Color(0xffe6e0e9)
|
||||||
|
|
||||||
fun signOut() {
|
fun signOut() {
|
||||||
EhCookieStore.signOut()
|
EhCookieStore.signOut()
|
||||||
Settings.avatar = null
|
Settings.displayName.value = null
|
||||||
Settings.displayName = null
|
|
||||||
Settings.gallerySite = EhUrl.SITE_E
|
Settings.gallerySite = EhUrl.SITE_E
|
||||||
Settings.needSignIn = true
|
Settings.needSignIn = true
|
||||||
}
|
}
|
||||||
@@ -156,16 +168,12 @@ object EhUtils {
|
|||||||
|
|
||||||
fun extractTitle(fullTitle: String?): String? {
|
fun extractTitle(fullTitle: String?): String? {
|
||||||
var title: String = fullTitle ?: return null
|
var title: String = fullTitle ?: return null
|
||||||
title = PATTERN_TITLE_PREFIX.matcher(title).replaceFirst("")
|
title = PATTERN_TITLE_PREFIX.replaceFirst(title, "")
|
||||||
title = PATTERN_TITLE_SUFFIX.matcher(title).replaceFirst("")
|
title = PATTERN_TITLE_SUFFIX.replaceFirst(title, "")
|
||||||
// Sometimes title is combined by romaji and english translation.
|
// Sometimes title is combined by romaji and english translation.
|
||||||
// Only need romaji.
|
// Only need romaji.
|
||||||
// TODO But not sure every '|' means that
|
// TODO But not sure every '|' means that
|
||||||
val index = title.indexOf('|')
|
return title.substringBeforeLast('|').trim().ifEmpty { null }
|
||||||
if (index >= 0) {
|
|
||||||
title = title.substring(0, index)
|
|
||||||
}
|
|
||||||
return title.ifEmpty { null }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun handleThumbUrlResolution(url: String?): String? {
|
fun handleThumbUrlResolution(url: String?): String? {
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ interface AbstractDownloadInfo {
|
|||||||
var legacy: Int
|
var legacy: Int
|
||||||
var time: Long
|
var time: Long
|
||||||
var label: String?
|
var label: String?
|
||||||
var position: Int
|
|
||||||
var speed: Long
|
var speed: Long
|
||||||
var remaining: Long
|
var remaining: Long
|
||||||
var finished: Int
|
var finished: Int
|
||||||
|
|||||||
@@ -20,18 +20,23 @@ import androidx.room.ColumnInfo
|
|||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
import androidx.room.Ignore
|
import androidx.room.Ignore
|
||||||
import androidx.room.PrimaryKey
|
import androidx.room.PrimaryKey
|
||||||
|
import androidx.room.TypeConverter
|
||||||
|
import androidx.room.TypeConverters
|
||||||
import com.hippo.ehviewer.client.data.GalleryInfo.Companion.NOT_FAVORITED
|
import com.hippo.ehviewer.client.data.GalleryInfo.Companion.NOT_FAVORITED
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
|
@Serializable
|
||||||
@Entity(tableName = "GALLERIES")
|
@Entity(tableName = "GALLERIES")
|
||||||
|
@TypeConverters(SimpleTagsConverter::class)
|
||||||
data class BaseGalleryInfo(
|
data class BaseGalleryInfo(
|
||||||
@PrimaryKey
|
@PrimaryKey
|
||||||
@ColumnInfo(name = "GID")
|
@ColumnInfo(name = "GID")
|
||||||
override var gid: Long = 0,
|
override var gid: Long,
|
||||||
|
|
||||||
@ColumnInfo(name = "TOKEN")
|
@ColumnInfo(name = "TOKEN")
|
||||||
override var token: String? = null,
|
override var token: String,
|
||||||
|
|
||||||
@ColumnInfo(name = "TITLE")
|
@ColumnInfo(name = "TITLE")
|
||||||
override var title: String? = null,
|
override var title: String? = null,
|
||||||
@@ -60,10 +65,10 @@ data class BaseGalleryInfo(
|
|||||||
@Ignore
|
@Ignore
|
||||||
override var rated: Boolean = false,
|
override var rated: Boolean = false,
|
||||||
|
|
||||||
@Ignore
|
@ColumnInfo(name = "SIMPLE_TAGS")
|
||||||
override var simpleTags: ArrayList<String>? = null,
|
override var simpleTags: List<String>? = null,
|
||||||
|
|
||||||
@Ignore
|
@ColumnInfo(name = "PAGES", defaultValue = "0")
|
||||||
override var pages: Int = 0,
|
override var pages: Int = 0,
|
||||||
|
|
||||||
@Ignore
|
@Ignore
|
||||||
@@ -84,5 +89,13 @@ data class BaseGalleryInfo(
|
|||||||
@Ignore
|
@Ignore
|
||||||
override var favoriteNote: String? = null,
|
override var favoriteNote: String? = null,
|
||||||
) : GalleryInfo, Parcelable {
|
) : GalleryInfo, Parcelable {
|
||||||
constructor() : this(0)
|
constructor() : this(0, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
class SimpleTagsConverter {
|
||||||
|
@TypeConverter
|
||||||
|
fun fromString(value: String) = value.split(", ")
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun toString(value: List<String>) = value.joinToString()
|
||||||
}
|
}
|
||||||
@@ -17,41 +17,38 @@ package com.hippo.ehviewer.client.data
|
|||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import com.hippo.ehviewer.client.EhUrl
|
import com.hippo.ehviewer.client.EhUrl
|
||||||
|
import com.hippo.ehviewer.client.addQueryParameter
|
||||||
import com.hippo.ehviewer.client.addQueryParameterIfNotBlank
|
import com.hippo.ehviewer.client.addQueryParameterIfNotBlank
|
||||||
import com.hippo.ehviewer.client.ehUrl
|
import com.hippo.ehviewer.client.ehUrl
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
class FavListUrlBuilder(
|
data class FavListUrlBuilder(
|
||||||
var mPrev: String? = null,
|
val favCat: Int = FAV_CAT_ALL,
|
||||||
var mNext: String? = null,
|
val keyword: String? = null,
|
||||||
var jumpTo: String? = null,
|
var jumpTo: String? = null,
|
||||||
var keyword: String? = null,
|
var prev: String? = null,
|
||||||
var favCat: Int = FAV_CAT_ALL,
|
var next: String? = null,
|
||||||
) : Parcelable {
|
) : Parcelable {
|
||||||
fun setIndex(index: String?, isNext: Boolean) {
|
fun setIndex(index: String?, isNext: Boolean) {
|
||||||
mNext = index.takeIf { isNext }
|
next = index.takeIf { isNext }
|
||||||
mPrev = index.takeUnless { isNext }
|
prev = index.takeUnless { isNext }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun build() = ehUrl {
|
fun build() = ehUrl(EhUrl.FAV_PATH) {
|
||||||
addPathSegment(EhUrl.FAV_PATH)
|
when {
|
||||||
if (isValidFavCat(favCat)) {
|
isValidFavCat(favCat) -> addQueryParameter("favcat", "$favCat")
|
||||||
addQueryParameter("favcat", "$favCat")
|
favCat == FAV_CAT_ALL -> addQueryParameter("favcat", "all")
|
||||||
} else if (favCat == FAV_CAT_ALL) {
|
|
||||||
addQueryParameter("favcat", "all")
|
|
||||||
}
|
}
|
||||||
addQueryParameterIfNotBlank("f_search", keyword)
|
addQueryParameterIfNotBlank("f_search", keyword)
|
||||||
addQueryParameterIfNotBlank("prev", mPrev)
|
addQueryParameterIfNotBlank("prev", prev)
|
||||||
addQueryParameterIfNotBlank("next", mNext)
|
addQueryParameterIfNotBlank("next", next)
|
||||||
addQueryParameterIfNotBlank("seek", jumpTo)
|
addQueryParameterIfNotBlank("seek", jumpTo)
|
||||||
}.toString()
|
}.buildString()
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val FAV_CAT_ALL = -1
|
const val FAV_CAT_ALL = -1
|
||||||
const val FAV_CAT_LOCAL = -2
|
const val FAV_CAT_LOCAL = -2
|
||||||
fun isValidFavCat(favCat: Int): Boolean {
|
fun isValidFavCat(favCat: Int) = favCat in 0..9
|
||||||
return favCat in 0..9
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2016 Hippo Seven
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package com.hippo.ehviewer.client.data
|
|
||||||
|
|
||||||
import android.os.Parcelable
|
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
class GalleryApiInfo(
|
|
||||||
var gid: Long = 0,
|
|
||||||
var token: String? = null,
|
|
||||||
var archiverKey: String? = null,
|
|
||||||
var title: String? = null,
|
|
||||||
var titleJpn: String? = null,
|
|
||||||
var category: Int = 0,
|
|
||||||
var thumb: String? = null,
|
|
||||||
var uploader: String? = null,
|
|
||||||
var posted: Long = 0,
|
|
||||||
var filecount: Int = 0,
|
|
||||||
var filesize: Long = 0,
|
|
||||||
var expunged: Boolean = false,
|
|
||||||
var rating: Float = 0f,
|
|
||||||
var torrentcount: Int = 0,
|
|
||||||
var tags: Array<String>?,
|
|
||||||
) : Parcelable
|
|
||||||
@@ -19,19 +19,18 @@ import android.os.Parcelable
|
|||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
class GalleryComment(
|
data class GalleryComment(
|
||||||
// 0 for uploader comment. can't vote
|
val id: Long,
|
||||||
var id: Long = 0,
|
val score: Int,
|
||||||
var score: Int = 0,
|
val editable: Boolean,
|
||||||
var editable: Boolean = false,
|
val voteUpAble: Boolean,
|
||||||
var voteUpAble: Boolean = false,
|
val voteUpEd: Boolean,
|
||||||
var voteUpEd: Boolean = false,
|
val voteDownAble: Boolean,
|
||||||
var voteDownAble: Boolean = false,
|
val voteDownEd: Boolean,
|
||||||
var voteDownEd: Boolean = false,
|
val uploader: Boolean,
|
||||||
var uploader: Boolean = false,
|
val voteState: String?,
|
||||||
var voteState: String? = null,
|
val time: Long,
|
||||||
var time: Long = 0,
|
val user: String?,
|
||||||
var user: String? = null,
|
val comment: String,
|
||||||
var comment: String? = null,
|
val lastEdited: Long,
|
||||||
var lastEdited: Long = 0,
|
|
||||||
) : Parcelable
|
) : Parcelable
|
||||||
@@ -19,7 +19,7 @@ import android.os.Parcelable
|
|||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
class GalleryCommentList(
|
data class GalleryCommentList(
|
||||||
var comments: Array<GalleryComment>,
|
val comments: List<GalleryComment>,
|
||||||
val hasMore: Boolean,
|
val hasMore: Boolean,
|
||||||
) : Parcelable
|
) : Parcelable
|
||||||
@@ -37,7 +37,7 @@ private val LANGUAGES = arrayOf(
|
|||||||
)
|
)
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
class GalleryDetail(
|
data class GalleryDetail(
|
||||||
val galleryInfo: BaseGalleryInfo = BaseGalleryInfo(),
|
val galleryInfo: BaseGalleryInfo = BaseGalleryInfo(),
|
||||||
var apiUid: Long = -1L,
|
var apiUid: Long = -1L,
|
||||||
var apiKey: String? = null,
|
var apiKey: String? = null,
|
||||||
@@ -45,19 +45,20 @@ class GalleryDetail(
|
|||||||
var torrentUrl: String? = null,
|
var torrentUrl: String? = null,
|
||||||
var archiveUrl: String? = null,
|
var archiveUrl: String? = null,
|
||||||
var parent: String? = null,
|
var parent: String? = null,
|
||||||
var newerVersions: ArrayList<BaseGalleryInfo> = ArrayList(),
|
var newerVersions: List<BaseGalleryInfo> = emptyList(),
|
||||||
var visible: String? = null,
|
var visible: String? = null,
|
||||||
var language: String? = null,
|
var language: String? = null,
|
||||||
var size: String? = null,
|
var size: String? = null,
|
||||||
var favoriteCount: Int = 0,
|
var favoriteCount: Int = 0,
|
||||||
var ratingCount: Int = 0,
|
var ratingCount: Int = 0,
|
||||||
val tags: Array<GalleryTagGroup>,
|
val tags: List<GalleryTagGroup>,
|
||||||
var comments: GalleryCommentList,
|
var comments: GalleryCommentList,
|
||||||
val previewPages: Int,
|
val previewPages: Int,
|
||||||
val previewList: List<GalleryPreview>,
|
val previewList: List<GalleryPreview>,
|
||||||
) : GalleryInfo by galleryInfo, Parcelable {
|
) : GalleryInfo by galleryInfo, Parcelable {
|
||||||
override fun generateSLang() {
|
fun fillInfo() {
|
||||||
val index = LANGUAGES.indexOf(language)
|
val index = LANGUAGES.indexOf(language)
|
||||||
if (index != -1) simpleLanguage = S_LANGS[index]
|
if (index != -1) simpleLanguage = S_LANGS[index]
|
||||||
|
simpleTags = tags.flatten()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -15,11 +15,11 @@
|
|||||||
*/
|
*/
|
||||||
package com.hippo.ehviewer.client.data
|
package com.hippo.ehviewer.client.data
|
||||||
|
|
||||||
import java.util.regex.Pattern
|
import moe.tarsin.kt.unreachable
|
||||||
|
|
||||||
interface GalleryInfo {
|
interface GalleryInfo {
|
||||||
var gid: Long
|
var gid: Long
|
||||||
var token: String?
|
var token: String
|
||||||
var title: String?
|
var title: String?
|
||||||
var titleJpn: String?
|
var titleJpn: String?
|
||||||
var thumbKey: String?
|
var thumbKey: String?
|
||||||
@@ -29,7 +29,7 @@ interface GalleryInfo {
|
|||||||
var disowned: Boolean
|
var disowned: Boolean
|
||||||
var rating: Float
|
var rating: Float
|
||||||
var rated: Boolean
|
var rated: Boolean
|
||||||
var simpleTags: ArrayList<String>?
|
var simpleTags: List<String>?
|
||||||
var pages: Int
|
var pages: Int
|
||||||
var thumbWidth: Int
|
var thumbWidth: Int
|
||||||
var thumbHeight: Int
|
var thumbHeight: Int
|
||||||
@@ -61,38 +61,38 @@ interface GalleryInfo {
|
|||||||
"NL",
|
"NL",
|
||||||
)
|
)
|
||||||
private val S_LANG_PATTERNS = arrayOf(
|
private val S_LANG_PATTERNS = arrayOf(
|
||||||
Pattern.compile(
|
Regex(
|
||||||
"[(\\[]eng(?:lish)?[)\\]]|英訳",
|
"[(\\[]eng(?:lish)?[)\\]]|英訳",
|
||||||
Pattern.CASE_INSENSITIVE,
|
RegexOption.IGNORE_CASE,
|
||||||
),
|
),
|
||||||
// [((\[]ch(?:inese)?[))\]]|[汉漢]化|中[国國][语語]|中文|中国翻訳
|
// [((\[]ch(?:inese)?[))\]]|[汉漢]化|中[国國][语語]|中文|中国翻訳
|
||||||
Pattern.compile(
|
Regex(
|
||||||
"[(\uFF08\\[]ch(?:inese)?[)\uFF09\\]]|[汉漢]化|中[国國][语語]|中文|中国翻訳",
|
"[(\uFF08\\[]ch(?:inese)?[)\uFF09\\]]|[汉漢]化|中[国國][语語]|中文|中国翻訳",
|
||||||
Pattern.CASE_INSENSITIVE,
|
RegexOption.IGNORE_CASE,
|
||||||
),
|
),
|
||||||
Pattern.compile(
|
Regex(
|
||||||
"[(\\[]spanish[)\\]]|[(\\[]Español[)\\]]|スペイン翻訳",
|
"[(\\[]spanish[)\\]]|[(\\[]Español[)\\]]|スペイン翻訳",
|
||||||
Pattern.CASE_INSENSITIVE,
|
RegexOption.IGNORE_CASE,
|
||||||
),
|
),
|
||||||
Pattern.compile("[(\\[]korean?[)\\]]|韓国翻訳", Pattern.CASE_INSENSITIVE),
|
Regex("[(\\[]korean?[)\\]]|韓国翻訳", RegexOption.IGNORE_CASE),
|
||||||
Pattern.compile("[(\\[]rus(?:sian)?[)\\]]|ロシア翻訳", Pattern.CASE_INSENSITIVE),
|
Regex("[(\\[]rus(?:sian)?[)\\]]|ロシア翻訳", RegexOption.IGNORE_CASE),
|
||||||
Pattern.compile("[(\\[]fr(?:ench)?[)\\]]|フランス翻訳", Pattern.CASE_INSENSITIVE),
|
Regex("[(\\[]fr(?:ench)?[)\\]]|フランス翻訳", RegexOption.IGNORE_CASE),
|
||||||
Pattern.compile("[(\\[]portuguese|ポルトガル翻訳", Pattern.CASE_INSENSITIVE),
|
Regex("[(\\[]portuguese|ポルトガル翻訳", RegexOption.IGNORE_CASE),
|
||||||
Pattern.compile(
|
Regex(
|
||||||
"[(\\[]thai(?: ภาษาไทย)?[)\\]]|แปลไทย|タイ翻訳",
|
"[(\\[]thai(?: ภาษาไทย)?[)\\]]|แปลไทย|タイ翻訳",
|
||||||
Pattern.CASE_INSENSITIVE,
|
RegexOption.IGNORE_CASE,
|
||||||
),
|
),
|
||||||
Pattern.compile("[(\\[]german[)\\]]|ドイツ翻訳", Pattern.CASE_INSENSITIVE),
|
Regex("[(\\[]german[)\\]]|ドイツ翻訳", RegexOption.IGNORE_CASE),
|
||||||
Pattern.compile("[(\\[]italiano?[)\\]]|イタリア翻訳", Pattern.CASE_INSENSITIVE),
|
Regex("[(\\[]italiano?[)\\]]|イタリア翻訳", RegexOption.IGNORE_CASE),
|
||||||
Pattern.compile(
|
Regex(
|
||||||
"[(\\[]vietnamese(?: Tiếng Việt)?[)\\]]|ベトナム翻訳",
|
"[(\\[]vietnamese(?: Tiếng Việt)?[)\\]]|ベトナム翻訳",
|
||||||
Pattern.CASE_INSENSITIVE,
|
RegexOption.IGNORE_CASE,
|
||||||
),
|
),
|
||||||
Pattern.compile("[(\\[]polish[)\\]]|ポーランド翻訳", Pattern.CASE_INSENSITIVE),
|
Regex("[(\\[]polish[)\\]]|ポーランド翻訳", RegexOption.IGNORE_CASE),
|
||||||
Pattern.compile("[(\\[]hun(?:garian)?[)\\]]|ハンガリー翻訳", Pattern.CASE_INSENSITIVE),
|
Regex("[(\\[]hun(?:garian)?[)\\]]|ハンガリー翻訳", RegexOption.IGNORE_CASE),
|
||||||
Pattern.compile("[(\\[]dutch[)\\]]|オランダ翻訳", Pattern.CASE_INSENSITIVE),
|
Regex("[(\\[]dutch[)\\]]|オランダ翻訳", RegexOption.IGNORE_CASE),
|
||||||
)
|
)
|
||||||
private val S_LANG_TAGS = arrayOf(
|
val S_LANG_TAGS = arrayOf(
|
||||||
"language:english",
|
"language:english",
|
||||||
"language:chinese",
|
"language:chinese",
|
||||||
"language:spanish",
|
"language:spanish",
|
||||||
@@ -122,7 +122,7 @@ interface GalleryInfo {
|
|||||||
|
|
||||||
private fun generateSLangFromTitle(title: String): String? {
|
private fun generateSLangFromTitle(title: String): String? {
|
||||||
for (i in S_LANGS.indices) {
|
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]
|
return S_LANGS[i]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -133,3 +133,15 @@ interface GalleryInfo {
|
|||||||
const val LOCAL_FAVORITED = -1
|
const val LOCAL_FAVORITED = -1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun GalleryInfo.findBaseInfo(): BaseGalleryInfo {
|
||||||
|
return when (this) {
|
||||||
|
is BaseGalleryInfo -> this
|
||||||
|
is GalleryDetail -> galleryInfo
|
||||||
|
else -> unreachable()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun GalleryInfo.asGalleryDetail(): GalleryDetail? {
|
||||||
|
return this as? GalleryDetail
|
||||||
|
}
|
||||||
@@ -19,7 +19,7 @@ import android.os.Parcelable
|
|||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
class GalleryTagGroup(
|
data class GalleryTagGroup(
|
||||||
val groupName: String,
|
val groupName: String,
|
||||||
private val mTagList: MutableList<String> = mutableListOf(),
|
private val tagList: MutableList<String> = mutableListOf(),
|
||||||
) : Parcelable, MutableList<String> by mTagList
|
) : Parcelable, MutableList<String> by tagList
|
||||||
@@ -17,13 +17,14 @@ package com.hippo.ehviewer.client.data
|
|||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import androidx.annotation.IntDef
|
import androidx.annotation.IntDef
|
||||||
|
import com.hippo.ehviewer.Settings
|
||||||
import com.hippo.ehviewer.client.EhUrl
|
import com.hippo.ehviewer.client.EhUrl
|
||||||
import com.hippo.ehviewer.client.EhUtils
|
import com.hippo.ehviewer.client.EhUtils
|
||||||
|
import com.hippo.ehviewer.client.addQueryParameter
|
||||||
import com.hippo.ehviewer.client.addQueryParameterIfNotBlank
|
import com.hippo.ehviewer.client.addQueryParameterIfNotBlank
|
||||||
import com.hippo.ehviewer.client.ehUrl
|
import com.hippo.ehviewer.client.ehUrl
|
||||||
import com.hippo.ehviewer.dao.QuickSearch
|
import com.hippo.ehviewer.dao.QuickSearch
|
||||||
import com.hippo.ehviewer.ui.main.AdvanceTable
|
import com.hippo.ehviewer.ui.main.AdvanceTable
|
||||||
import com.hippo.ehviewer.util.encodeUTF8
|
|
||||||
import com.hippo.ehviewer.util.toIntOrDefault
|
import com.hippo.ehviewer.util.toIntOrDefault
|
||||||
import java.io.UnsupportedEncodingException
|
import java.io.UnsupportedEncodingException
|
||||||
import java.net.URLDecoder
|
import java.net.URLDecoder
|
||||||
@@ -35,7 +36,9 @@ data class ListUrlBuilder(
|
|||||||
var mode: Int = MODE_NORMAL,
|
var mode: Int = MODE_NORMAL,
|
||||||
private var mPrev: String? = null,
|
private var mPrev: String? = null,
|
||||||
var mNext: String? = null,
|
var mNext: String? = null,
|
||||||
|
// Reset to null after initial loading
|
||||||
var mJumpTo: String? = null,
|
var mJumpTo: String? = null,
|
||||||
|
private var mRange: Int = 0,
|
||||||
var category: Int = EhUtils.NONE,
|
var category: Int = EhUtils.NONE,
|
||||||
private var mKeyword: String? = null,
|
private var mKeyword: String? = null,
|
||||||
var hash: String? = null,
|
var hash: String? = null,
|
||||||
@@ -44,44 +47,32 @@ data class ListUrlBuilder(
|
|||||||
var pageFrom: Int = -1,
|
var pageFrom: Int = -1,
|
||||||
var pageTo: Int = -1,
|
var pageTo: Int = -1,
|
||||||
var imagePath: String? = null,
|
var imagePath: String? = null,
|
||||||
var isUseSimilarityScan: Boolean = false,
|
|
||||||
var isOnlySearchCovers: Boolean = false,
|
|
||||||
) : Parcelable {
|
) : Parcelable {
|
||||||
|
|
||||||
fun reset() {
|
fun setIndex(index: String, isNext: Boolean = true) {
|
||||||
mode = MODE_NORMAL
|
mNext = index.takeIf { isNext }
|
||||||
mPrev = null
|
mPrev = index.takeUnless { isNext }
|
||||||
mNext = null
|
mRange = 0
|
||||||
mJumpTo = null
|
|
||||||
this.category = EhUtils.NONE
|
|
||||||
mKeyword = null
|
|
||||||
advanceSearch = -1
|
|
||||||
minRating = -1
|
|
||||||
pageFrom = -1
|
|
||||||
pageTo = -1
|
|
||||||
imagePath = null
|
|
||||||
isUseSimilarityScan = false
|
|
||||||
isOnlySearchCovers = false
|
|
||||||
hash = null
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setIndex(index: String?, isNext: Boolean = true) {
|
|
||||||
mNext = index?.takeIf { isNext }
|
|
||||||
mPrev = index?.takeUnless { isNext }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setJumpTo(jumpTo: Int) {
|
fun setJumpTo(jumpTo: Int) {
|
||||||
mJumpTo = jumpTo.takeUnless { it == 0 }?.toString()
|
mJumpTo = jumpTo.takeUnless { it == 0 }?.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setRange(range: Int) {
|
||||||
|
mRange = range
|
||||||
|
mPrev = null
|
||||||
|
mNext = null
|
||||||
|
mJumpTo = null
|
||||||
|
}
|
||||||
|
|
||||||
var keyword: String?
|
var keyword: String?
|
||||||
get() = if (MODE_UPLOADER == mode) "uploader:$mKeyword" else mKeyword
|
get() = if (MODE_UPLOADER == mode) "uploader:$mKeyword" else mKeyword
|
||||||
set(keyword) {
|
set(keyword) {
|
||||||
mKeyword = keyword
|
mKeyword = keyword
|
||||||
}
|
}
|
||||||
|
|
||||||
fun set(q: QuickSearch) {
|
constructor(q: QuickSearch) : this() {
|
||||||
reset()
|
|
||||||
mode = q.mode
|
mode = q.mode
|
||||||
this.category = q.category
|
this.category = q.category
|
||||||
mKeyword = q.keyword
|
mKeyword = q.keyword
|
||||||
@@ -89,7 +80,7 @@ data class ListUrlBuilder(
|
|||||||
minRating = q.minRating
|
minRating = q.minRating
|
||||||
pageFrom = q.pageFrom
|
pageFrom = q.pageFrom
|
||||||
pageTo = q.pageTo
|
pageTo = q.pageTo
|
||||||
mNext = q.name.substringAfterLast('@', "").takeIf { it.isNotEmpty() }
|
mNext = q.name.substringAfterLast('@', "").ifEmpty { null }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toQuickSearch(name: String): QuickSearch {
|
fun toQuickSearch(name: String): QuickSearch {
|
||||||
@@ -134,9 +125,8 @@ data class ListUrlBuilder(
|
|||||||
/**
|
/**
|
||||||
* @param query xxx=yyy&mmm=nnn
|
* @param query xxx=yyy&mmm=nnn
|
||||||
*/
|
*/
|
||||||
fun setQuery(query: String?) {
|
constructor(query: String?) : this() {
|
||||||
// TODO page
|
// TODO page
|
||||||
reset()
|
|
||||||
if (query.isNullOrEmpty()) {
|
if (query.isNullOrEmpty()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -271,31 +261,23 @@ data class ListUrlBuilder(
|
|||||||
|
|
||||||
fun build(): String {
|
fun build(): String {
|
||||||
return when (mode) {
|
return when (mode) {
|
||||||
MODE_NORMAL, MODE_SUBSCRIPTION -> ehUrl {
|
MODE_NORMAL, MODE_SUBSCRIPTION -> ehUrl(EhUrl.WATCHED_PATH.takeIf { mode == MODE_SUBSCRIPTION }) {
|
||||||
if (mode == MODE_SUBSCRIPTION) addPathSegment(EhUrl.WATCHED_PATH)
|
if (category > 0) {
|
||||||
if (category != EhUtils.NONE) {
|
addQueryParameter("f_cats", (category.inv() and EhUtils.ALL_CATEGORY).toString())
|
||||||
addEncodedQueryParameter("f_cats", (category.inv() and EhUtils.ALL_CATEGORY).toString())
|
|
||||||
}
|
}
|
||||||
addQueryParameterIfNotBlank("f_shash", hash)
|
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("prev", mPrev)
|
||||||
addQueryParameterIfNotBlank("next", mNext)
|
addQueryParameterIfNotBlank("next", mNext)
|
||||||
addQueryParameterIfNotBlank("seek", mJumpTo)
|
addQueryParameterIfNotBlank("seek", mJumpTo)
|
||||||
// Search key
|
addQueryParameterIfNotBlank("range", mRange.takeIf { it > 0 }?.toString())
|
||||||
// the settings of ub:UrlBuilder may be overwritten by following Advance search
|
|
||||||
mKeyword?.split('|')?.forEachIndexed { idx, kwd ->
|
|
||||||
val keyword = kwd.trim { it <= ' ' }
|
|
||||||
when (idx) {
|
|
||||||
0 -> addQueryParameterIfNotBlank("f_search", keyword)
|
|
||||||
|
|
||||||
else -> keyword.indexOf(':').takeIf { it >= 0 }?.run {
|
|
||||||
val key = keyword.substring(0, this).trim { it <= ' ' }
|
|
||||||
val value = keyword.substring(this + 1).trim { it <= ' ' }
|
|
||||||
addQueryParameterIfNotBlank(key, value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Advance search
|
// Advance search
|
||||||
if (advanceSearch != -1) {
|
if (advanceSearch > 0 || minRating > 0 || pageFrom > 0 || pageTo > 0) {
|
||||||
addQueryParameter("advsearch", "1")
|
addQueryParameter("advsearch", "1")
|
||||||
if (advanceSearch and AdvanceTable.SH != 0) {
|
if (advanceSearch and AdvanceTable.SH != 0) {
|
||||||
addQueryParameter("f_sh", "on")
|
addQueryParameter("f_sh", "on")
|
||||||
@@ -313,74 +295,44 @@ data class ListUrlBuilder(
|
|||||||
addQueryParameter("f_sft", "on")
|
addQueryParameter("f_sft", "on")
|
||||||
}
|
}
|
||||||
// Set min star
|
// Set min star
|
||||||
if (minRating != -1) {
|
if (minRating > 0) {
|
||||||
addQueryParameter("f_sr", "on")
|
addQueryParameter("f_sr", "on")
|
||||||
addQueryParameter("f_srdd", "$minRating")
|
addQueryParameter("f_srdd", "$minRating")
|
||||||
}
|
}
|
||||||
// Pages
|
// Pages
|
||||||
if (pageFrom != -1 || pageTo != -1) {
|
if (pageFrom > 0 || pageTo > 0) {
|
||||||
addQueryParameter("f_sp", "on")
|
addQueryParameter("f_sp", "on")
|
||||||
addQueryParameterIfNotBlank(
|
addQueryParameterIfNotBlank(
|
||||||
"f_spf",
|
"f_spf",
|
||||||
pageFrom.takeUnless { it == -1 }?.toString(),
|
pageFrom.takeIf { it > 0 }?.toString(),
|
||||||
)
|
)
|
||||||
addQueryParameterIfNotBlank(
|
addQueryParameterIfNotBlank(
|
||||||
"f_spt",
|
"f_spt",
|
||||||
pageTo.takeUnless { it == -1 }?.toString(),
|
pageTo.takeIf { it > 0 }?.toString(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.toString()
|
}.buildString()
|
||||||
|
|
||||||
MODE_UPLOADER -> {
|
MODE_UPLOADER, MODE_TAG -> {
|
||||||
buildString {
|
val path = if (mode == MODE_UPLOADER) "uploader" else "tag"
|
||||||
append(EhUrl.host)
|
ehUrl(listOf(path, requireNotNull(mKeyword))) {
|
||||||
mKeyword?.let {
|
addQueryParameterIfNotBlank("prev", mPrev)
|
||||||
append("uploader/").append(encodeUTF8(it))
|
addQueryParameterIfNotBlank("next", mNext)
|
||||||
}
|
addQueryParameterIfNotBlank("seek", mJumpTo)
|
||||||
mPrev?.let {
|
addQueryParameterIfNotBlank("range", mRange.takeIf { it > 0 }?.toString())
|
||||||
append("?prev=").append(it)
|
}.buildString()
|
||||||
}
|
|
||||||
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_WHATS_HOT -> EhUrl.popularUrl
|
||||||
MODE_IMAGE_SEARCH -> EhUrl.imageSearchUrl
|
MODE_IMAGE_SEARCH -> ehUrl {
|
||||||
|
addQueryParameter("f_shash", requireNotNull(hash))
|
||||||
|
}.buildString()
|
||||||
MODE_TOPLIST -> {
|
MODE_TOPLIST -> {
|
||||||
buildString {
|
ehUrl("toplist.php", EhUrl.DOMAIN_E) {
|
||||||
append(EhUrl.HOST_E)
|
addQueryParameter("tl", requireNotNull(mKeyword))
|
||||||
append("toplist.php?tl=")
|
addQueryParameterIfNotBlank("p", mJumpTo)
|
||||||
mKeyword.orEmpty().let {
|
}.buildString()
|
||||||
append(encodeUTF8(it))
|
|
||||||
}
|
|
||||||
mJumpTo?.let {
|
|
||||||
append("&p=").append(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> throw IllegalStateException("Unexpected value: $mode")
|
else -> throw IllegalStateException("Unexpected value: $mode")
|
||||||
|
|||||||
@@ -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.InsufficientFundsException
|
||||||
import com.hippo.ehviewer.client.exception.NoHAtHClientException
|
import com.hippo.ehviewer.client.exception.NoHAtHClientException
|
||||||
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
import org.jsoup.Jsoup
|
import org.jsoup.Jsoup
|
||||||
|
|
||||||
object ArchiveParser {
|
object ArchiveParser {
|
||||||
@@ -33,7 +34,7 @@ object ArchiveParser {
|
|||||||
private const val ERROR_INSUFFICIENT_FUNDS =
|
private const val ERROR_INSUFFICIENT_FUNDS =
|
||||||
"You do not have enough funds to download this archive."
|
"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 m = PATTERN_HATH_FORM.find(body)!!
|
||||||
val paramOr = m.groupValues[1]
|
val paramOr = m.groupValues[1]
|
||||||
val archiveList = ArrayList<Archive>()
|
val archiveList = ArrayList<Archive>()
|
||||||
@@ -48,7 +49,7 @@ object ArchiveParser {
|
|||||||
}.onSuccess {
|
}.onSuccess {
|
||||||
archiveList.add(it)
|
archiveList.add(it)
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
it.printStackTrace()
|
logcat(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -58,14 +59,12 @@ object ArchiveParser {
|
|||||||
val item = Archive(res, name, size, cost, true)
|
val item = Archive(res, name, size, cost, true)
|
||||||
archiveList.add(item)
|
archiveList.add(item)
|
||||||
}
|
}
|
||||||
val result = Result(paramOr, archiveList, null)
|
val funds = maybeFunds ?: PATTERN_CURRENT_FUNDS.find(body)!!.groupValues.run {
|
||||||
PATTERN_CURRENT_FUNDS.find(body)?.groupValues?.run {
|
|
||||||
val fundsGP = ParserUtils.parseInt(get(1), 0)
|
val fundsGP = ParserUtils.parseInt(get(1), 0)
|
||||||
val fundsC = ParserUtils.parseInt(get(2), 0)
|
val fundsC = ParserUtils.parseInt(get(2), 0)
|
||||||
val funds = HomeParser.Funds(fundsGP, fundsC)
|
Funds("%,d".format(fundsGP), "%,d".format(fundsC))
|
||||||
result.funds = funds
|
|
||||||
}
|
}
|
||||||
return result
|
return Result(paramOr, archiveList, funds)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun parseArchiveUrl(body: String): String? {
|
fun parseArchiveUrl(body: String): String? {
|
||||||
@@ -79,13 +78,13 @@ object ArchiveParser {
|
|||||||
// TODO: Check more errors
|
// TODO: Check more errors
|
||||||
}
|
}
|
||||||
|
|
||||||
class Archive(
|
data class Result(val paramOr: String, val archiveList: List<Archive>, val funds: Funds)
|
||||||
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?)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
@@ -1,32 +1,29 @@
|
|||||||
package com.hippo.ehviewer.client.parser
|
package com.hippo.ehviewer.client.parser
|
||||||
|
|
||||||
import com.hippo.ehviewer.client.exception.NotLoggedInException
|
|
||||||
import com.hippo.ehviewer.client.exception.ParseException
|
import com.hippo.ehviewer.client.exception.ParseException
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import moe.tarsin.coroutines.runSuspendCatching
|
||||||
|
|
||||||
|
@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
|
||||||
|
}
|
||||||
|
|
||||||
object FavoritesParser {
|
object FavoritesParser {
|
||||||
suspend fun parse(body: String): Result {
|
fun parse(body: ByteBuffer): FavParserResult {
|
||||||
if (body.contains("This page requires you to log on.</p>")) {
|
return runSuspendCatching {
|
||||||
throw NotLoggedInException()
|
unmarshalParsingAs<FavParserResult>(body, ::parseFav)
|
||||||
}
|
|
||||||
val catArray = arrayOfNulls<String>(10)
|
|
||||||
val countArray = runCatching {
|
|
||||||
parseFav(body, catArray).also { check(it.isNotEmpty()) }
|
|
||||||
}.getOrElse {
|
}.getOrElse {
|
||||||
throw ParseException("Parse favorites error", it)
|
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private external fun parseFav(body: String, favCat: Array<String?>): IntArray
|
private external fun parseFav(body: ByteBuffer, limit: Int = body.limit()): Int
|
||||||
@@ -33,7 +33,7 @@ object GalleryApiParser {
|
|||||||
titleJpn = item.titleJpn.unescapeXml()
|
titleJpn = item.titleJpn.unescapeXml()
|
||||||
category = getCategory(item.category)
|
category = getCategory(item.category)
|
||||||
thumbKey = getThumbKey(handleThumbUrlResolution(item.thumb)!!)
|
thumbKey = getThumbKey(handleThumbUrlResolution(item.thumb)!!)
|
||||||
uploader = item.uploader.unescapeXml()
|
uploader = item.uploader?.unescapeXml()
|
||||||
posted = ParserUtils.formatDate(item.posted * 1000)
|
posted = ParserUtils.formatDate(item.posted * 1000)
|
||||||
rating = item.rating
|
rating = item.rating
|
||||||
simpleTags = item.tags
|
simpleTags = item.tags
|
||||||
@@ -54,11 +54,12 @@ object GalleryApiParser {
|
|||||||
val titleJpn: String,
|
val titleJpn: String,
|
||||||
val category: String,
|
val category: String,
|
||||||
val thumb: String,
|
val thumb: String,
|
||||||
val uploader: String,
|
// Null in some old galleries
|
||||||
|
val uploader: String?,
|
||||||
val posted: Long,
|
val posted: Long,
|
||||||
@SerialName("filecount")
|
@SerialName("filecount")
|
||||||
val pages: Int,
|
val pages: Int,
|
||||||
val rating: Float,
|
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.exception.PiningException
|
||||||
import com.hippo.ehviewer.client.getThumbKey
|
import com.hippo.ehviewer.client.getThumbKey
|
||||||
import com.hippo.ehviewer.util.ExceptionUtils
|
import com.hippo.ehviewer.util.ExceptionUtils
|
||||||
|
import com.hippo.ehviewer.util.toEpochMillis
|
||||||
import com.hippo.ehviewer.util.toFloatOrDefault
|
import com.hippo.ehviewer.util.toFloatOrDefault
|
||||||
import com.hippo.ehviewer.util.toIntOrDefault
|
import com.hippo.ehviewer.util.toIntOrDefault
|
||||||
import com.hippo.ehviewer.util.trimAnd
|
import com.hippo.ehviewer.util.trimAnd
|
||||||
import com.hippo.ehviewer.util.unescapeXml
|
import com.hippo.ehviewer.util.unescapeXml
|
||||||
import java.time.Instant
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
import java.time.ZoneOffset
|
import kotlinx.datetime.LocalDateTime
|
||||||
import java.time.format.DateTimeFormatter
|
import kotlinx.datetime.format.MonthNames
|
||||||
import java.util.Locale
|
import kotlinx.datetime.format.char
|
||||||
import org.jsoup.Jsoup
|
import org.jsoup.Jsoup
|
||||||
import org.jsoup.nodes.Document
|
import org.jsoup.nodes.Document
|
||||||
import org.jsoup.nodes.Element
|
import org.jsoup.nodes.Element
|
||||||
@@ -76,10 +77,20 @@ object GalleryDetailParser {
|
|||||||
private val PATTERN_NEWER_DATE = Regex(", added (.+?)<br />")
|
private val PATTERN_NEWER_DATE = Regex(", added (.+?)<br />")
|
||||||
private val PATTERN_FAVORITE_SLOT =
|
private val PATTERN_FAVORITE_SLOT =
|
||||||
Regex("/fav.png\\); background-position:0px -(\\d+)px")
|
Regex("/fav.png\\); background-position:0px -(\\d+)px")
|
||||||
private val EMPTY_GALLERY_TAG_GROUP_ARRAY = arrayOf<GalleryTagGroup>()
|
private val EMPTY_GALLERY_COMMENT_LIST = GalleryCommentList(emptyList(), false)
|
||||||
private val EMPTY_GALLERY_COMMENT_ARRAY = GalleryCommentList(arrayOf(), false)
|
|
||||||
private val WEB_COMMENT_DATE_FORMAT = DateTimeFormatter
|
// dd MMMM yyyy, HH:mm
|
||||||
.ofPattern("dd MMMM yyyy, HH:mm", Locale.US).withZone(ZoneOffset.UTC)
|
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 =
|
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>"
|
"<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>"
|
private const val PINING_STRING = "<p>This gallery is pining for the fjords.</p>"
|
||||||
@@ -94,8 +105,7 @@ object GalleryDetailParser {
|
|||||||
|
|
||||||
// Error info
|
// Error info
|
||||||
PATTERN_ERROR.find(body)?.run { throw EhException(groupValues[1]) }
|
PATTERN_ERROR.find(body)?.run { throw EhException(groupValues[1]) }
|
||||||
// Temporary workaround, see https://github.com/jhy/jsoup/issues/1850
|
val document = Jsoup.parse(body)
|
||||||
val document = Jsoup.parse(body.replace("del>", "s>"))
|
|
||||||
val galleryDetail = GalleryDetail(
|
val galleryDetail = GalleryDetail(
|
||||||
tags = parseTagGroups(document),
|
tags = parseTagGroups(document),
|
||||||
comments = parseComments(document),
|
comments = parseComments(document),
|
||||||
@@ -104,8 +114,8 @@ object GalleryDetailParser {
|
|||||||
)
|
)
|
||||||
parseDetail(galleryDetail, document, body)
|
parseDetail(galleryDetail, document, body)
|
||||||
|
|
||||||
// Generate simpleLanguage for local favorites
|
// Fill info for database
|
||||||
galleryDetail.generateSLang()
|
galleryDetail.fillInfo()
|
||||||
return galleryDetail
|
return galleryDetail
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,15 +231,10 @@ object GalleryDetailParser {
|
|||||||
// newer version
|
// newer version
|
||||||
d.getElementById("gnd")?.run {
|
d.getElementById("gnd")?.run {
|
||||||
val dates = PATTERN_NEWER_DATE.findAll(body).map { it.groupValues[1] }.toList()
|
val dates = PATTERN_NEWER_DATE.findAll(body).map { it.groupValues[1] }.toList()
|
||||||
select("a").forEachIndexed { index, element ->
|
gd.newerVersions = select("a").mapIndexedNotNull { index, element ->
|
||||||
val gi = BaseGalleryInfo()
|
|
||||||
val result = GalleryDetailUrlParser.parse(element.attr("href"))
|
val result = GalleryDetailUrlParser.parse(element.attr("href"))
|
||||||
if (result != null) {
|
result?.run {
|
||||||
gi.gid = result.gid
|
BaseGalleryInfo(gid, token, element.text().trim(), posted = dates[index])
|
||||||
gi.token = result.token
|
|
||||||
gi.title = element.text().trim()
|
|
||||||
gi.posted = dates[index]
|
|
||||||
gd.newerVersions.add(gi)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -300,7 +305,7 @@ object GalleryDetailParser {
|
|||||||
if (group.size > 0) group else null
|
if (group.size > 0) group else null
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
ExceptionUtils.throwIfFatal(e)
|
ExceptionUtils.throwIfFatal(e)
|
||||||
e.printStackTrace()
|
logcat(e)
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -308,81 +313,77 @@ object GalleryDetailParser {
|
|||||||
/**
|
/**
|
||||||
* Parse tag groups with html parser
|
* Parse tag groups with html parser
|
||||||
*/
|
*/
|
||||||
private fun parseTagGroups(document: Document): Array<GalleryTagGroup> {
|
private fun parseTagGroups(document: Document): List<GalleryTagGroup> {
|
||||||
return try {
|
return try {
|
||||||
val taglist = document.getElementById("taglist")!!
|
val taglist = document.getElementById("taglist")!!
|
||||||
val tagGroups = taglist.child(0).child(0).children()
|
val tagGroups = taglist.child(0).child(0).children()
|
||||||
parseTagGroups(tagGroups)
|
parseTagGroups(tagGroups)
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
ExceptionUtils.throwIfFatal(e)
|
ExceptionUtils.throwIfFatal(e)
|
||||||
e.printStackTrace()
|
logcat(e)
|
||||||
EMPTY_GALLERY_TAG_GROUP_ARRAY
|
emptyList()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun parseTagGroups(trs: Elements): Array<GalleryTagGroup> {
|
private fun parseTagGroups(trs: Elements): List<GalleryTagGroup> {
|
||||||
return try {
|
return try {
|
||||||
trs.mapNotNull { parseTagGroup(it) }.toTypedArray()
|
trs.mapNotNull { parseTagGroup(it) }
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
ExceptionUtils.throwIfFatal(e)
|
ExceptionUtils.throwIfFatal(e)
|
||||||
e.printStackTrace()
|
logcat(e)
|
||||||
EMPTY_GALLERY_TAG_GROUP_ARRAY
|
emptyList()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun parseComment(element: Element): GalleryComment? {
|
private suspend fun parseComment(element: Element): GalleryComment? {
|
||||||
return try {
|
return try {
|
||||||
val comment = GalleryComment()
|
|
||||||
// Id
|
// Id
|
||||||
val a = element.previousElementSibling()
|
val a = element.previousElementSibling()
|
||||||
val name = a!!.attr("name")
|
val name = a!!.attr("name")
|
||||||
comment.id = name trimAnd { substring(1).toInt().toLong() }
|
val id = name trimAnd { substring(1).toInt().toLong() }
|
||||||
// Editable, vote up and vote down
|
// Editable, vote up and vote down
|
||||||
val c4 = element.getElementsByClass("c4").first()
|
val c4 = element.getElementsByClass("c4").first()
|
||||||
|
var voteUpAble = false
|
||||||
|
var voteUpEd = false
|
||||||
|
var voteDownAble = false
|
||||||
|
var voteDownEd = false
|
||||||
|
var editable = false
|
||||||
|
var uploader = false
|
||||||
if (null != c4) {
|
if (null != c4) {
|
||||||
if ("Uploader Comment" == c4.text()) {
|
if ("Uploader Comment" == c4.text()) {
|
||||||
comment.uploader = true
|
uploader = true
|
||||||
}
|
}
|
||||||
for (e in c4.children()) {
|
for (e in c4.children()) {
|
||||||
when (e.text()) {
|
when (e.text()) {
|
||||||
"Vote+" -> {
|
"Vote+" -> {
|
||||||
comment.voteUpAble = true
|
voteUpAble = true
|
||||||
comment.voteUpEd = e.attr("style").trim().isNotEmpty()
|
voteUpEd = e.attr("style").trim().isNotEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
"Vote-" -> {
|
"Vote-" -> {
|
||||||
comment.voteDownAble = true
|
voteDownAble = true
|
||||||
comment.voteDownEd = e.attr("style").trim().isNotEmpty()
|
voteDownEd = e.attr("style").trim().isNotEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
"Edit" -> comment.editable = true
|
"Edit" -> editable = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Vote state
|
// Vote state
|
||||||
val c7 = element.getElementsByClass("c7").first()
|
val c7 = element.getElementsByClass("c7").first()
|
||||||
if (null != c7) {
|
val voteState = c7?.text()?.trim()
|
||||||
comment.voteState = c7.text().trim()
|
|
||||||
}
|
|
||||||
// Score
|
// Score
|
||||||
val c5 = element.getElementsByClass("c5").first()
|
val c5 = element.getElementsByClass("c5").first()
|
||||||
if (null != c5) {
|
val score = c5?.children()?.first()?.text()?.trim()?.toIntOrDefault(0) ?: 0
|
||||||
val es = c5.children()
|
|
||||||
if (!es.isEmpty()) {
|
|
||||||
comment.score = es[0].text().trim().toIntOrDefault(0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// time
|
// time
|
||||||
val c3 = element.getElementsByClass("c3").first()
|
val c3 = element.getElementsByClass("c3").first()
|
||||||
val temp = c3!!.ownText()
|
val temp = c3!!.ownText()
|
||||||
val time = if (temp.endsWith(':')) {
|
val (timeStr, user) = if (temp.endsWith(':')) {
|
||||||
// user
|
temp.substring("Posted on ".length, temp.length - " by:".length) to c3.child(0).text()
|
||||||
comment.user = c3.child(0).text()
|
|
||||||
temp.substring("Posted on ".length, temp.length - " by:".length)
|
|
||||||
} else {
|
} else {
|
||||||
temp.substring("Posted on ".length)
|
temp.substring("Posted on ".length) to null
|
||||||
}
|
}
|
||||||
comment.time = Instant.from(WEB_COMMENT_DATE_FORMAT.parse(time)).toEpochMilli()
|
val time = WEB_COMMENT_DATE_FORMAT.parse(timeStr).toEpochMillis()
|
||||||
// comment
|
// comment
|
||||||
val c6 = element.getElementsByClass("c6").first()
|
val c6 = element.getElementsByClass("c6").first()
|
||||||
// fix underline support
|
// fix underline support
|
||||||
@@ -391,27 +392,24 @@ object GalleryDetailParser {
|
|||||||
e.tagName("u")
|
e.tagName("u")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
comment.comment = c6.html()
|
val commentHtml = c6.html()
|
||||||
// filter comment
|
// filter comment
|
||||||
if (!comment.uploader) {
|
if (!uploader) {
|
||||||
val sEhFilter = EhFilter
|
val sEhFilter = EhFilter
|
||||||
if (comment.score <= Settings.commentThreshold || sEhFilter.filterCommenter(comment.user!!) || sEhFilter.filterComment(comment.comment!!)) {
|
if (score <= Settings.commentThreshold || sEhFilter.filterCommenter(user.orEmpty()) || sEhFilter.filterComment(commentHtml)) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// last edited
|
// last edited
|
||||||
val c8 = element.getElementsByClass("c8").first()
|
val c8 = element.getElementsByClass("c8").first()
|
||||||
if (c8 != null) {
|
val lastEdited = c8?.children()?.first()?.run { WEB_COMMENT_DATE_FORMAT.parse(text()).toEpochMillis() } ?: 0
|
||||||
val e = c8.children().first()
|
GalleryComment(
|
||||||
if (e != null) {
|
id, score, editable, voteUpAble, voteUpEd, voteDownAble,
|
||||||
comment.lastEdited =
|
voteDownEd, uploader, voteState, time, user, commentHtml, lastEdited,
|
||||||
Instant.from(WEB_COMMENT_DATE_FORMAT.parse(e.text())).toEpochMilli()
|
)
|
||||||
}
|
|
||||||
}
|
|
||||||
comment
|
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
ExceptionUtils.throwIfFatal(e)
|
ExceptionUtils.throwIfFatal(e)
|
||||||
e.printStackTrace()
|
logcat(e)
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -420,6 +418,8 @@ object GalleryDetailParser {
|
|||||||
* Parse comments with html parser
|
* Parse comments with html parser
|
||||||
*/
|
*/
|
||||||
suspend fun parseComments(document: Document): GalleryCommentList {
|
suspend fun parseComments(document: Document): GalleryCommentList {
|
||||||
|
// Disable pretty print to get comments in raw html
|
||||||
|
document.outputSettings().prettyPrint(false)
|
||||||
return try {
|
return try {
|
||||||
val cdiv = document.getElementById("cdiv")!!
|
val cdiv = document.getElementById("cdiv")!!
|
||||||
val c1s = cdiv.getElementsByClass("c1")
|
val c1s = cdiv.getElementsByClass("c1")
|
||||||
@@ -438,11 +438,11 @@ object GalleryDetailParser {
|
|||||||
},
|
},
|
||||||
chd!!,
|
chd!!,
|
||||||
)
|
)
|
||||||
GalleryCommentList(list.toTypedArray(), hasMore)
|
GalleryCommentList(list, hasMore)
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
ExceptionUtils.throwIfFatal(e)
|
ExceptionUtils.throwIfFatal(e)
|
||||||
e.printStackTrace()
|
logcat(e)
|
||||||
EMPTY_GALLERY_COMMENT_ARRAY
|
EMPTY_GALLERY_COMMENT_LIST
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,29 +16,23 @@
|
|||||||
package com.hippo.ehviewer.client.parser
|
package com.hippo.ehviewer.client.parser
|
||||||
|
|
||||||
import com.hippo.ehviewer.client.EhUrl
|
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>
|
* Like http://exhentai.org/g/1234567/a1b2c3d4e5<br></br>
|
||||||
*/
|
*/
|
||||||
object GalleryDetailUrlParser {
|
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})",
|
"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? {
|
fun parse(url: String?, strict: Boolean = true): Result? {
|
||||||
url ?: return null
|
url ?: return null
|
||||||
val pattern = if (strict) URL_STRICT_PATTERN else URL_PATTERN
|
val pattern = if (strict) URL_STRICT_PATTERN else URL_PATTERN
|
||||||
val m = pattern.matcher(url)
|
return pattern.find(url)?.destructured?.let { (gid, token) ->
|
||||||
return if (m.find()) {
|
Result(gid.toLong(), token)
|
||||||
val gid = m.group(1)!!.toLongOrDefault(0).takeIf { it > 0 }
|
|
||||||
gid?.let { Result(it, m.group(2)!!) }
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Result(val gid: Long, val token: String)
|
data class Result(val gid: Long, val token: String)
|
||||||
}
|
}
|
||||||
@@ -1,40 +1,26 @@
|
|||||||
package com.hippo.ehviewer.client.parser
|
package com.hippo.ehviewer.client.parser
|
||||||
|
|
||||||
import com.hippo.ehviewer.EhDB
|
|
||||||
import com.hippo.ehviewer.R
|
|
||||||
import com.hippo.ehviewer.client.data.BaseGalleryInfo
|
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 com.hippo.ehviewer.client.exception.ParseException
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import moe.tarsin.coroutines.runSuspendCatching
|
||||||
|
|
||||||
object GalleryListParser {
|
object GalleryListParser {
|
||||||
suspend fun parse(body: String) = runCatching {
|
fun parse(body: ByteBuffer) = runSuspendCatching {
|
||||||
parseGalleryInfoList(body).apply {
|
unmarshalParsingAs<GalleryListResult>(body, ::parseGalleryInfoList)
|
||||||
galleryInfoList.onEach {
|
|
||||||
if (it.favoriteSlot == NOT_FAVORITED && EhDB.containLocalFavorites(it.gid)) {
|
|
||||||
it.favoriteSlot = LOCAL_FAVORITED
|
|
||||||
}
|
|
||||||
it.generateSLang()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.getOrElse {
|
}.getOrElse {
|
||||||
if (body.contains("<p>You do not have any watched tags")) {
|
|
||||||
throw EhException(R.string.gallery_list_empty_hit_subscription)
|
|
||||||
}
|
|
||||||
if (body.contains("No hits found</p>")) {
|
|
||||||
throw EhException(R.string.gallery_list_empty_hit)
|
|
||||||
}
|
|
||||||
throw ParseException("Can't parse gallery list", it)
|
throw ParseException("Can't parse gallery list", it)
|
||||||
}
|
}
|
||||||
|
|
||||||
val emptyResult = GalleryListResult(null, null, arrayListOf())
|
val emptyResult = GalleryListResult(null, null, arrayListOf())
|
||||||
}
|
}
|
||||||
|
|
||||||
class GalleryListResult(
|
@Serializable
|
||||||
|
data class GalleryListResult(
|
||||||
val prev: String?,
|
val prev: String?,
|
||||||
val next: String?,
|
val next: String?,
|
||||||
val galleryInfoList: ArrayList<BaseGalleryInfo>,
|
val galleryInfoList: ArrayList<BaseGalleryInfo>,
|
||||||
)
|
)
|
||||||
|
|
||||||
private external fun parseGalleryInfoList(e: String): 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 android.os.Build
|
||||||
import com.hippo.ehviewer.client.EhUrl
|
import com.hippo.ehviewer.client.EhUrl
|
||||||
import com.hippo.ehviewer.client.data.ListUrlBuilder
|
import com.hippo.ehviewer.client.data.ListUrlBuilder
|
||||||
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
import java.io.UnsupportedEncodingException
|
import java.io.UnsupportedEncodingException
|
||||||
import java.net.MalformedURLException
|
import java.net.MalformedURLException
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
@@ -42,9 +43,7 @@ object GalleryListUrlParser {
|
|||||||
}
|
}
|
||||||
val path = url.path ?: return null
|
val path = url.path ?: return null
|
||||||
return if (PATH_NORMAL == path || path.isEmpty()) {
|
return if (PATH_NORMAL == path || path.isEmpty()) {
|
||||||
val builder = ListUrlBuilder()
|
ListUrlBuilder(url.query)
|
||||||
builder.setQuery(url.query)
|
|
||||||
builder
|
|
||||||
} else if (path.startsWith(PATH_UPLOADER)) {
|
} else if (path.startsWith(PATH_UPLOADER)) {
|
||||||
parseUploader(path)
|
parseUploader(path)
|
||||||
} else if (path.startsWith(PATH_TAG)) {
|
} else if (path.startsWith(PATH_TAG)) {
|
||||||
@@ -57,10 +56,7 @@ object GalleryListUrlParser {
|
|||||||
} catch (e: NumberFormatException) {
|
} catch (e: NumberFormatException) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
val builder = ListUrlBuilder()
|
ListUrlBuilder(url.query).also { it.category = category }
|
||||||
builder.setQuery(url.query)
|
|
||||||
builder.category = category
|
|
||||||
builder
|
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
@@ -82,7 +78,7 @@ object GalleryListUrlParser {
|
|||||||
try {
|
try {
|
||||||
URLDecoder.decode(uploader, StandardCharsets.UTF_8.displayName())
|
URLDecoder.decode(uploader, StandardCharsets.UTF_8.displayName())
|
||||||
} catch (e: UnsupportedEncodingException) {
|
} catch (e: UnsupportedEncodingException) {
|
||||||
e.printStackTrace()
|
logcat(e)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -111,7 +107,7 @@ object GalleryListUrlParser {
|
|||||||
try {
|
try {
|
||||||
URLDecoder.decode(tag, StandardCharsets.UTF_8.displayName())
|
URLDecoder.decode(tag, StandardCharsets.UTF_8.displayName())
|
||||||
} catch (e: UnsupportedEncodingException) {
|
} catch (e: UnsupportedEncodingException) {
|
||||||
e.printStackTrace()
|
logcat(e)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
package com.hippo.ehviewer.client.parser
|
package com.hippo.ehviewer.client.parser
|
||||||
|
|
||||||
import com.hippo.ehviewer.util.ExceptionUtils
|
import com.hippo.ehviewer.util.ExceptionUtils
|
||||||
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
import org.jsoup.Jsoup
|
import org.jsoup.Jsoup
|
||||||
|
|
||||||
object GalleryNotAvailableParser {
|
object GalleryNotAvailableParser {
|
||||||
@@ -27,7 +28,7 @@ object GalleryNotAvailableParser {
|
|||||||
e!!.child(0).html().replace("<br>", "\n")
|
e!!.child(0).html().replace("<br>", "\n")
|
||||||
}.getOrElse {
|
}.getOrElse {
|
||||||
ExceptionUtils.throwIfFatal(it)
|
ExceptionUtils.throwIfFatal(it)
|
||||||
it.printStackTrace()
|
logcat(it)
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,56 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2019 Hippo Seven
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package com.hippo.ehviewer.client.parser
|
|
||||||
|
|
||||||
import com.hippo.ehviewer.client.exception.ParseException
|
|
||||||
import com.hippo.ehviewer.client.parseAs
|
|
||||||
import com.hippo.ehviewer.util.unescapeXml
|
|
||||||
import kotlinx.serialization.SerialName
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
|
|
||||||
object GalleryPageApiParser {
|
|
||||||
private val PATTERN_IMAGE_URL = Regex("<img[^>]*src=\"([^\"]+)\" style")
|
|
||||||
private val PATTERN_SKIP_HATH_KEY = Regex("onclick=\"return nl\\('([^)]+)'\\)")
|
|
||||||
private val PATTERN_ORIGIN_IMAGE_URL = Regex("<a href=\"([^\"]+)fullimg.php([^\"]+)\">")
|
|
||||||
|
|
||||||
fun parse(body: String) = runCatching {
|
|
||||||
val res = body.parseAs<JsonResult>()
|
|
||||||
val imageUrl = PATTERN_IMAGE_URL.find(res.imageUrl)?.run {
|
|
||||||
groupValues[1].trim().unescapeXml()
|
|
||||||
}
|
|
||||||
val skipHathKey = PATTERN_SKIP_HATH_KEY.find(res.skipHathKeyAndOriginImageUrl)?.run {
|
|
||||||
groupValues[1].trim().unescapeXml()
|
|
||||||
}
|
|
||||||
val originImageUrl = PATTERN_ORIGIN_IMAGE_URL.find(res.skipHathKeyAndOriginImageUrl)?.run {
|
|
||||||
groupValues[1].unescapeXml() + "fullimg.php" + groupValues[2].unescapeXml()
|
|
||||||
}
|
|
||||||
check(!imageUrl.isNullOrEmpty())
|
|
||||||
Result(imageUrl, skipHathKey, originImageUrl)
|
|
||||||
}.getOrElse { e ->
|
|
||||||
runCatching { body.parseAs<Error>() }.onSuccess { throw ParseException(it.error) }
|
|
||||||
throw ParseException("Parse image url and skip hath key error", e)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
private data class JsonResult(
|
|
||||||
@SerialName("i3")
|
|
||||||
val imageUrl: String,
|
|
||||||
@SerialName("i6")
|
|
||||||
val skipHathKeyAndOriginImageUrl: String,
|
|
||||||
)
|
|
||||||
|
|
||||||
class Result(val imageUrl: String, val skipHathKey: String?, val originImageUrl: String?)
|
|
||||||
}
|
|
||||||
@@ -17,46 +17,32 @@ package com.hippo.ehviewer.client.parser
|
|||||||
|
|
||||||
import com.hippo.ehviewer.client.exception.ParseException
|
import com.hippo.ehviewer.client.exception.ParseException
|
||||||
import com.hippo.ehviewer.util.unescapeXml
|
import com.hippo.ehviewer.util.unescapeXml
|
||||||
import java.util.regex.Pattern
|
|
||||||
|
|
||||||
object GalleryPageParser {
|
object GalleryPageParser {
|
||||||
private val PATTERN_IMAGE_URL = Pattern.compile("<img[^>]*src=\"([^\"]+)\" style")
|
private val PATTERN_IMAGE_URL = Regex("<img[^>]*src=\"([^\"]+)\" style")
|
||||||
private val PATTERN_SKIP_HATH_KEY = Pattern.compile("onclick=\"return nl\\('([^)]+)'\\)")
|
private val PATTERN_SKIP_HATH_KEY = Regex("onclick=\"return nl\\('([^)]+)'\\)")
|
||||||
private val PATTERN_ORIGIN_IMAGE_URL =
|
private val PATTERN_ORIGIN_IMAGE_URL = Regex("<a href=\"([^\"]+/fullimg/[^\"]+)\">")
|
||||||
Pattern.compile("<a href=\"([^\"]+)fullimg.php([^\"]+)\">")
|
|
||||||
|
|
||||||
// TODO Not sure about the size of show keys
|
// TODO Not sure about the size of show keys
|
||||||
private val PATTERN_SHOW_KEY = Pattern.compile("var showkey=\"([0-9a-z]+)\";")
|
private val PATTERN_SHOW_KEY = Regex("var showkey=\"([0-9a-z]+)\";")
|
||||||
|
|
||||||
fun parse(body: String): Result {
|
fun parse(body: String): Result {
|
||||||
var m = PATTERN_IMAGE_URL.matcher(body)
|
val imageUrl = PATTERN_IMAGE_URL.find(body)?.run {
|
||||||
val imageUrl = if (m.find()) {
|
groupValues[1].unescapeXml()
|
||||||
m.group(1)!!.trim().unescapeXml()
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
}
|
||||||
m = PATTERN_SKIP_HATH_KEY.matcher(body)
|
val skipHathKey = PATTERN_SKIP_HATH_KEY.find(body)?.run {
|
||||||
val skipHathKey = if (m.find()) {
|
groupValues[1]
|
||||||
m.group(1)!!.trim().unescapeXml()
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
}
|
||||||
m = PATTERN_ORIGIN_IMAGE_URL.matcher(body)
|
val originImageUrl = PATTERN_ORIGIN_IMAGE_URL.find(body)?.run {
|
||||||
val originImageUrl = if (m.find()) {
|
groupValues[1].unescapeXml()
|
||||||
m.group(1)!!.unescapeXml() + "fullimg.php" + m.group(2)!!.unescapeXml()
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
}
|
||||||
m = PATTERN_SHOW_KEY.matcher(body)
|
val showKey = PATTERN_SHOW_KEY.find(body)?.run {
|
||||||
val showKey = if (m.find()) {
|
groupValues[1]
|
||||||
m.group(1)
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
}
|
||||||
return if (!imageUrl.isNullOrEmpty() && !showKey.isNullOrEmpty()) {
|
return if (!imageUrl.isNullOrEmpty()) {
|
||||||
Result(imageUrl, skipHathKey, originImageUrl, showKey)
|
Result(imageUrl, skipHathKey, originImageUrl, showKey)
|
||||||
} else {
|
} else {
|
||||||
throw ParseException("Parse image url and show error")
|
throw ParseException("Parse image url error")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,6 +50,6 @@ object GalleryPageParser {
|
|||||||
val imageUrl: String,
|
val imageUrl: String,
|
||||||
val skipHathKey: String?,
|
val skipHathKey: String?,
|
||||||
val originImageUrl: String?,
|
val originImageUrl: String?,
|
||||||
val showKey: String,
|
val showKey: String?,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -16,34 +16,21 @@
|
|||||||
package com.hippo.ehviewer.client.parser
|
package com.hippo.ehviewer.client.parser
|
||||||
|
|
||||||
import com.hippo.ehviewer.client.EhUrl
|
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
|
* Like http://exhentai.org/s/91ea4b6d89/901103-12
|
||||||
*/
|
*/
|
||||||
object GalleryPageUrlParser {
|
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+)",
|
"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? {
|
fun parse(url: String?, strict: Boolean = true): Result? {
|
||||||
url ?: return null
|
url ?: return null
|
||||||
val pattern = if (strict) URL_STRICT_PATTERN else URL_PATTERN
|
val pattern = if (strict) URL_STRICT_PATTERN else URL_PATTERN
|
||||||
val m = pattern.matcher(url)
|
return pattern.find(url)?.destructured?.let { (pToken, gid, page) ->
|
||||||
return if (m.find()) {
|
Result(gid.toLong(), pToken, page.toInt() - 1)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,13 +5,14 @@ import com.hippo.ehviewer.client.exception.InsufficientFundsException
|
|||||||
import com.hippo.ehviewer.client.exception.ParseException
|
import com.hippo.ehviewer.client.exception.ParseException
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
object HomeParser {
|
object HomeParser {
|
||||||
private val PATTERN_FUNDS = Regex("Available: ([\\d,]+) Credits.*Available: ([\\d,]+) kGP", RegexOption.DOT_MATCHES_ALL)
|
private val PATTERN_FUNDS = Regex("Available: ([\\d,]+) Credits.*Available: ([\\d,]+) kGP", RegexOption.DOT_MATCHES_ALL)
|
||||||
private const val INSUFFICIENT_FUNDS = "Insufficient funds."
|
private const val INSUFFICIENT_FUNDS = "Insufficient funds."
|
||||||
|
|
||||||
fun parse(body: ByteBuffer) = runCatching {
|
fun parse(body: ByteBuffer) = runCatching {
|
||||||
parseLimit(body)
|
unmarshalParsingAs<Limits>(body, ::parseLimit)
|
||||||
}.getOrElse { throw ParseException("Parse image limits error", it) }
|
}.getOrElse { throw ParseException("Parse image limits error", it) }
|
||||||
|
|
||||||
fun parseResetLimits(body: String) {
|
fun parseResetLimits(body: String) {
|
||||||
@@ -24,17 +25,20 @@ object HomeParser {
|
|||||||
PATTERN_FUNDS.find(body)?.groupValues?.run {
|
PATTERN_FUNDS.find(body)?.groupValues?.run {
|
||||||
val fundsC = ParserUtils.parseInt(get(1), 0)
|
val fundsC = ParserUtils.parseInt(get(1), 0)
|
||||||
val fundsGP = ParserUtils.parseInt(get(2), 0) * 1000
|
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")
|
throw ParseException("Parse funds error")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class Funds(val fundsGP: Int, val fundsC: Int) : Parcelable
|
data class Result(val limits: Limits, val funds: Funds) : Parcelable
|
||||||
class Result(val limits: Limits, val funds: Funds)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
|
data class Funds(val gp: String, val credit: String) : Parcelable
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
@Serializable
|
||||||
data class Limits(val current: Int, val maximum: Int, val resetCost: Int) : Parcelable
|
data class Limits(val current: Int, val maximum: Int, val resetCost: Int) : Parcelable
|
||||||
|
|
||||||
private external fun parseLimit(body: ByteBuffer, limit: Int = body.limit()): Limits
|
private external fun parseLimit(body: ByteBuffer, limit: Int = body.limit()): Int
|
||||||
@@ -15,21 +15,34 @@
|
|||||||
*/
|
*/
|
||||||
package com.hippo.ehviewer.client.parser
|
package com.hippo.ehviewer.client.parser
|
||||||
|
|
||||||
|
import com.hippo.ehviewer.util.toEpochMillis
|
||||||
import com.hippo.ehviewer.util.toIntOrDefault
|
import com.hippo.ehviewer.util.toIntOrDefault
|
||||||
|
import com.hippo.ehviewer.util.toLocalDateTime
|
||||||
import com.hippo.ehviewer.util.toLongOrDefault
|
import com.hippo.ehviewer.util.toLongOrDefault
|
||||||
import com.hippo.ehviewer.util.unescapeXml
|
import com.hippo.ehviewer.util.unescapeXml
|
||||||
import java.time.Instant
|
import kotlinx.datetime.LocalDateTime
|
||||||
import java.time.ZoneOffset
|
import kotlinx.datetime.format.char
|
||||||
import java.time.format.DateTimeFormatter
|
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
object ParserUtils {
|
object ParserUtils {
|
||||||
private val formatter =
|
// yyyy-MM-dd HH:mm
|
||||||
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm", Locale.US).withZone(ZoneOffset.UTC)
|
private val formatter = LocalDateTime.Format {
|
||||||
|
year()
|
||||||
|
char('-')
|
||||||
|
monthNumber()
|
||||||
|
char('-')
|
||||||
|
dayOfMonth()
|
||||||
|
char(' ')
|
||||||
|
hour()
|
||||||
|
char(':')
|
||||||
|
minute()
|
||||||
|
}
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
fun formatDate(time: Long): String {
|
fun formatDate(time: Long): String {
|
||||||
return formatter.format(Instant.ofEpochMilli(time))
|
return formatter.format(time.toLocalDateTime())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun parseDate(str: String): Long {
|
||||||
|
return formatter.parse(str).toEpochMillis()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun trim(str: String?): String {
|
fun trim(str: String?): String {
|
||||||
|
|||||||
@@ -15,14 +15,13 @@
|
|||||||
*/
|
*/
|
||||||
package com.hippo.ehviewer.client.parser
|
package com.hippo.ehviewer.client.parser
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import com.hippo.ehviewer.client.EhUrl
|
import com.hippo.ehviewer.client.EhUrl
|
||||||
import com.hippo.ehviewer.client.exception.ParseException
|
import com.hippo.ehviewer.client.exception.ParseException
|
||||||
import com.hippo.ehviewer.util.ExceptionUtils
|
import com.hippo.ehviewer.util.ExceptionUtils
|
||||||
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
import org.jsoup.Jsoup
|
import org.jsoup.Jsoup
|
||||||
|
|
||||||
object ProfileParser {
|
object ProfileParser {
|
||||||
private val TAG = ProfileParser::class.java.simpleName
|
|
||||||
fun parse(body: String): Result {
|
fun parse(body: String): Result {
|
||||||
return runCatching {
|
return runCatching {
|
||||||
val d = Jsoup.parse(body)
|
val d = Jsoup.parse(body)
|
||||||
@@ -31,7 +30,7 @@ object ProfileParser {
|
|||||||
val avatar = runCatching {
|
val avatar = runCatching {
|
||||||
val avatar =
|
val avatar =
|
||||||
profilename.nextElementSibling()!!.nextElementSibling()!!.child(0).attr("src")
|
profilename.nextElementSibling()!!.nextElementSibling()!!.child(0).attr("src")
|
||||||
if (avatar.isNullOrEmpty()) {
|
if (avatar.isEmpty()) {
|
||||||
null
|
null
|
||||||
} else if (!avatar.startsWith("http")) {
|
} else if (!avatar.startsWith("http")) {
|
||||||
EhUrl.URL_FORUMS + avatar
|
EhUrl.URL_FORUMS + avatar
|
||||||
@@ -40,7 +39,7 @@ object ProfileParser {
|
|||||||
}
|
}
|
||||||
}.getOrElse {
|
}.getOrElse {
|
||||||
ExceptionUtils.throwIfFatal(it)
|
ExceptionUtils.throwIfFatal(it)
|
||||||
Log.i(TAG, "No avatar")
|
logcat { "No avatar" }
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
Result(displayName, avatar)
|
Result(displayName, avatar)
|
||||||
|
|||||||
@@ -17,26 +17,19 @@ package com.hippo.ehviewer.client.parser
|
|||||||
|
|
||||||
import com.hippo.ehviewer.client.exception.EhException
|
import com.hippo.ehviewer.client.exception.EhException
|
||||||
import com.hippo.ehviewer.client.exception.ParseException
|
import com.hippo.ehviewer.client.exception.ParseException
|
||||||
import java.util.regex.Pattern
|
|
||||||
|
|
||||||
object SignInParser {
|
object SignInParser {
|
||||||
private val NAME_PATTERN = Pattern.compile("<p>You are now logged in as: (.+?)<")
|
private val NAME_PATTERN = Regex("<p>You are now logged in as: (.+?)<")
|
||||||
private val ERROR_PATTERN = Pattern.compile(
|
private val ERROR_PATTERN = Regex(
|
||||||
"<h4>The error returned was:</h4>\\s*<p>(.+?)</p>" +
|
"<h4>The error returned was:</h4>\\s*<p>(.+?)</p>" +
|
||||||
"|<span class=\"postcolor\">(.+?)</span>",
|
"|<span class=\"postcolor\">(.+?)</span>",
|
||||||
)
|
)
|
||||||
|
|
||||||
fun parse(body: String): String {
|
fun parse(body: String): String {
|
||||||
var m = NAME_PATTERN.matcher(body)
|
return NAME_PATTERN.find(body)?.let {
|
||||||
return if (m.find()) {
|
it.groupValues[1]
|
||||||
m.group(1)!!
|
} ?: ERROR_PATTERN.find(body)?.let {
|
||||||
} else {
|
throw EhException(it.groupValues[1].ifEmpty { it.groupValues[2] })
|
||||||
m = ERROR_PATTERN.matcher(body)
|
} ?: throw ParseException("Can't parse sign in")
|
||||||
if (m.find()) {
|
|
||||||
throw EhException(m.group(1) ?: m.group(2))
|
|
||||||
} else {
|
|
||||||
throw ParseException("Can't parse sign in")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,25 +2,31 @@ package com.hippo.ehviewer.client.parser
|
|||||||
|
|
||||||
import com.hippo.ehviewer.client.exception.ParseException
|
import com.hippo.ehviewer.client.exception.ParseException
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
object TorrentParser {
|
object TorrentParser {
|
||||||
fun parse(body: ByteBuffer) = runCatching {
|
fun parse(body: ByteBuffer) = runCatching {
|
||||||
parseTorrent(body)
|
unmarshalParsingAs<TorrentResult>(body, ::parseTorrent)
|
||||||
}.getOrElse { throw ParseException("Can't parse torrent list", it) }
|
}.getOrElse {
|
||||||
|
throw ParseException("Can't parse torrent list", it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Torrent(
|
@Serializable
|
||||||
|
data class Torrent(
|
||||||
|
val outdated: Boolean,
|
||||||
val posted: String,
|
val posted: String,
|
||||||
val size: String,
|
val size: String,
|
||||||
val seeds: Int,
|
val seeds: Int,
|
||||||
val peers: Int,
|
val peers: Int,
|
||||||
val downloads: Int,
|
val downloads: Int,
|
||||||
|
val uploader: String,
|
||||||
val url: String,
|
val url: String,
|
||||||
val name: String,
|
val name: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun Torrent.format() = "[$posted] $name [$size] [↑$seeds ↓$peers ✓$downloads]"
|
fun Torrent.format() = "↑$seeds ↓$peers ✓$downloads"
|
||||||
|
|
||||||
typealias TorrentResult = ArrayList<Torrent>
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
@file:Suppress("BlockingMethodInNonBlockingContext")
|
|
||||||
|
|
||||||
package com.hippo.ehviewer.coil
|
|
||||||
|
|
||||||
import android.net.Uri
|
|
||||||
import android.webkit.MimeTypeMap
|
|
||||||
import coil.ComponentRegistry
|
|
||||||
import coil.ImageLoader
|
|
||||||
import coil.decode.DataSource
|
|
||||||
import coil.decode.ImageSource
|
|
||||||
import coil.fetch.FetchResult
|
|
||||||
import coil.fetch.Fetcher
|
|
||||||
import coil.fetch.SourceResult
|
|
||||||
import coil.request.Options
|
|
||||||
import com.hippo.ehviewer.cronet.copyToChannel
|
|
||||||
import com.hippo.ehviewer.cronet.cronetRequest
|
|
||||||
import com.hippo.ehviewer.cronet.execute
|
|
||||||
import com.hippo.ehviewer.cronet.noCache
|
|
||||||
import java.io.RandomAccessFile
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A HttpUriFetcher impl use Cronet for jvmheapless IO
|
|
||||||
* cacheHeader is never respected | recorded since thumb is immutable
|
|
||||||
*/
|
|
||||||
class CronetHttpUriFetcher(private val data: String, private val options: Options, private val imageLoader: ImageLoader) : Fetcher {
|
|
||||||
override suspend fun fetch(): FetchResult {
|
|
||||||
val diskCacheKey = options.diskCacheKey ?: data
|
|
||||||
val diskCache = requireNotNull(imageLoader.diskCache)
|
|
||||||
val snapshot = diskCache.openSnapshot(diskCacheKey) ?: run {
|
|
||||||
val success = cronetRequest(data) {
|
|
||||||
noCache()
|
|
||||||
}.execute {
|
|
||||||
diskCache.suspendEdit(diskCacheKey) {
|
|
||||||
RandomAccessFile(data.toFile(), "rw").use {
|
|
||||||
copyToChannel(it.channel)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
check(success)
|
|
||||||
// diskcache snapshot MUST exist here
|
|
||||||
requireNotNull(diskCache.openSnapshot(diskCacheKey))
|
|
||||||
}
|
|
||||||
return SourceResult(
|
|
||||||
source = ImageSource(snapshot.data, diskCache.fileSystem, diskCacheKey, snapshot),
|
|
||||||
mimeType = getMimeType(data),
|
|
||||||
dataSource = DataSource.DISK,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun ComponentRegistry.Builder.installCronetHttpUriFetcher() = add { data: Uri, options, loader ->
|
|
||||||
if (data.scheme != "http" && data.scheme != "https") return@add null
|
|
||||||
CronetHttpUriFetcher(data.toString(), options, loader)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getMimeType(url: String) = MimeTypeMap.getSingleton().getMimeTypeFromUrl(url)
|
|
||||||
|
|
||||||
private fun MimeTypeMap.getMimeTypeFromUrl(url: String?): String? {
|
|
||||||
if (url.isNullOrBlank()) return null
|
|
||||||
val extension = url
|
|
||||||
.substringBeforeLast('#') // Strip the fragment.
|
|
||||||
.substringBeforeLast('?') // Strip the query.
|
|
||||||
.substringAfterLast('/') // Get the last path segment.
|
|
||||||
.substringAfterLast('.', missingDelimiterValue = "") // Get the file extension.
|
|
||||||
return getMimeTypeFromExtension(extension)
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
package com.hippo.ehviewer.coil
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import androidx.compose.ui.unit.IntRect
|
||||||
|
import coil3.BitmapImage
|
||||||
|
import coil3.Extras
|
||||||
|
import coil3.Image
|
||||||
|
import coil3.asCoilImage
|
||||||
|
import coil3.getExtra
|
||||||
|
import coil3.intercept.Interceptor
|
||||||
|
import coil3.intercept.Interceptor.Chain
|
||||||
|
import coil3.request.ImageRequest
|
||||||
|
import coil3.request.ImageResult
|
||||||
|
import coil3.request.SuccessResult
|
||||||
|
import coil3.request.allowHardware
|
||||||
|
import com.hippo.ehviewer.image.detectBorder
|
||||||
|
import com.hippo.ehviewer.util.isAtLeastO
|
||||||
|
|
||||||
|
private const val CROP_THRESHOLD = 0.75f
|
||||||
|
private const val RATIO_THRESHOLD = 2
|
||||||
|
|
||||||
|
private val maybeCropBorderKey = Extras.Key(default = false)
|
||||||
|
|
||||||
|
fun ImageRequest.Builder.maybeCropBorder(enable: Boolean) = apply {
|
||||||
|
extras[maybeCropBorderKey] = enable
|
||||||
|
}
|
||||||
|
|
||||||
|
val ImageRequest.maybeCropBorder: Boolean
|
||||||
|
get() = getExtra(maybeCropBorderKey)
|
||||||
|
|
||||||
|
class BitmapImageWithRect(
|
||||||
|
val rect: IntRect,
|
||||||
|
val image: Image,
|
||||||
|
) : Image by image
|
||||||
|
|
||||||
|
object CropBorderInterceptor : Interceptor {
|
||||||
|
override suspend fun intercept(chain: Chain): ImageResult {
|
||||||
|
val req = chain.request
|
||||||
|
if (req.maybeCropBorder) {
|
||||||
|
val hw = req.allowHardware && isAtLeastO
|
||||||
|
val result = if (hw) {
|
||||||
|
val nonHw = req.newBuilder().allowHardware(false).build()
|
||||||
|
chain.withRequest(nonHw).proceed()
|
||||||
|
} else {
|
||||||
|
chain.proceed()
|
||||||
|
}
|
||||||
|
if (result is SuccessResult) {
|
||||||
|
val image = result.image
|
||||||
|
if (image is BitmapImage) {
|
||||||
|
val bitmap = image.bitmap
|
||||||
|
val ratio = if (image.height > image.width) {
|
||||||
|
image.height / image.width
|
||||||
|
} else {
|
||||||
|
image.width / image.height
|
||||||
|
}
|
||||||
|
val rect = if (ratio < RATIO_THRESHOLD) {
|
||||||
|
val array = detectBorder(bitmap)
|
||||||
|
val minWidth = image.width * CROP_THRESHOLD
|
||||||
|
val minHeight = image.height * CROP_THRESHOLD
|
||||||
|
IntRect(array[0], array[1], array[2], array[3]).takeIf {
|
||||||
|
it.width > minWidth && it.height > minHeight
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
val maybeHwImage = if (hw) {
|
||||||
|
// Upload to Graphical Buffer to accelerate render
|
||||||
|
// May fail if the image is too long
|
||||||
|
bitmap.copy(Bitmap.Config.HARDWARE, false)?.run {
|
||||||
|
bitmap.recycle()
|
||||||
|
asCoilImage()
|
||||||
|
} ?: image
|
||||||
|
} else {
|
||||||
|
image
|
||||||
|
}
|
||||||
|
val new = if (rect != null) {
|
||||||
|
BitmapImageWithRect(rect, maybeHwImage)
|
||||||
|
} else {
|
||||||
|
maybeHwImage
|
||||||
|
}
|
||||||
|
return result.copy(image = new, request = result.request, dataSource = result.dataSource)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
return chain.proceed()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
package com.hippo.ehviewer.coil
|
package com.hippo.ehviewer.coil
|
||||||
|
|
||||||
import coil.disk.DiskCache
|
import coil3.disk.DiskCache
|
||||||
|
|
||||||
inline fun DiskCache.edit(key: String, crossinline block: DiskCache.Editor.() -> Unit): Boolean {
|
inline fun DiskCache.edit(key: String, crossinline block: DiskCache.Editor.() -> Unit): Boolean {
|
||||||
val editor = openEditor(key) ?: return false
|
val editor = openEditor(key) ?: return false
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package com.hippo.ehviewer.coil
|
||||||
|
|
||||||
|
import coil3.intercept.Interceptor
|
||||||
|
import coil3.request.ImageResult
|
||||||
|
import coil3.request.SuccessResult
|
||||||
|
import com.hippo.ehviewer.EhApplication.Companion.imageCache
|
||||||
|
import com.hippo.ehviewer.download.downloadLocation
|
||||||
|
import com.hippo.ehviewer.spider.DownloadInfoMagics.decodeMagicRequestOrUrl
|
||||||
|
import com.hippo.ehviewer.util.sendTo
|
||||||
|
import com.hippo.unifile.asUniFile
|
||||||
|
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||||
|
|
||||||
|
object DownloadThumbInterceptor : Interceptor {
|
||||||
|
// TODO: Remove this after a few releases
|
||||||
|
private const val LEGACY_THUMB_FILE = ".thumb"
|
||||||
|
private const val THUMB_FILE = "thumb.jpg"
|
||||||
|
override suspend fun intercept(chain: Interceptor.Chain): ImageResult {
|
||||||
|
val magicOrUrl = chain.request.data as? String
|
||||||
|
if (magicOrUrl != null) {
|
||||||
|
val (url, location) = decodeMagicRequestOrUrl(magicOrUrl)
|
||||||
|
if (location != null) {
|
||||||
|
val thumb = withIOContext {
|
||||||
|
val legacyThumb = downloadLocation / location / LEGACY_THUMB_FILE
|
||||||
|
if (legacyThumb.isFile) {
|
||||||
|
legacyThumb.apply { renameTo(THUMB_FILE) }
|
||||||
|
} else {
|
||||||
|
downloadLocation / location / THUMB_FILE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (withIOContext { thumb.isFile }) {
|
||||||
|
val new = chain.request.newBuilder().data(thumb.uri.toString()).build()
|
||||||
|
val result = chain.withRequest(new).proceed()
|
||||||
|
if (result is SuccessResult) return result
|
||||||
|
}
|
||||||
|
val new = chain.request.newBuilder().data(url).build()
|
||||||
|
val result = chain.withRequest(new).proceed()
|
||||||
|
if (result is SuccessResult) {
|
||||||
|
withIOContext {
|
||||||
|
// Accessing the recreated file immediately after deleting it throws
|
||||||
|
// FileNotFoundException, so we just overwrite the existing file.
|
||||||
|
if (thumb.ensureFile()) {
|
||||||
|
val key = requireNotNull(chain.request.memoryCacheKey)
|
||||||
|
imageCache.read(key) {
|
||||||
|
data.asUniFile() sendTo thumb
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return chain.proceed()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,22 +2,36 @@ package com.hippo.ehviewer.coil
|
|||||||
|
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.graphics.drawable.ColorDrawable
|
import android.graphics.drawable.ColorDrawable
|
||||||
import coil.decode.DecodeResult
|
import coil3.asCoilImage
|
||||||
import coil.decode.Decoder
|
import coil3.decode.DecodeResult
|
||||||
import coil.request.CachePolicy
|
import coil3.decode.Decoder
|
||||||
import coil.request.ImageRequest
|
import coil3.network.NetworkHeaders
|
||||||
import coil.size.Size
|
import coil3.network.httpHeaders
|
||||||
|
import coil3.request.CachePolicy
|
||||||
|
import coil3.request.ImageRequest
|
||||||
|
import coil3.size.Size
|
||||||
|
import com.hippo.ehviewer.Settings
|
||||||
|
import com.hippo.ehviewer.client.CHROME_ACCEPT
|
||||||
|
import com.hippo.ehviewer.client.CHROME_ACCEPT_LANGUAGE
|
||||||
import com.hippo.ehviewer.client.data.GalleryInfo
|
import com.hippo.ehviewer.client.data.GalleryInfo
|
||||||
import com.hippo.ehviewer.client.data.GalleryPreview
|
import com.hippo.ehviewer.client.data.GalleryPreview
|
||||||
import com.hippo.ehviewer.client.data.NormalGalleryPreview
|
import com.hippo.ehviewer.client.data.NormalGalleryPreview
|
||||||
import com.hippo.ehviewer.client.thumbUrl
|
|
||||||
import com.hippo.ehviewer.client.url
|
import com.hippo.ehviewer.client.url
|
||||||
|
import com.hippo.ehviewer.spider.DownloadInfoMagics.encodeMagicRequestOrUrl
|
||||||
|
import io.ktor.http.HttpHeaders
|
||||||
|
|
||||||
|
private val header = NetworkHeaders.Builder().apply {
|
||||||
|
add(HttpHeaders.UserAgent, Settings.userAgent)
|
||||||
|
add(HttpHeaders.Accept, CHROME_ACCEPT)
|
||||||
|
add(HttpHeaders.AcceptLanguage, CHROME_ACCEPT_LANGUAGE)
|
||||||
|
}.build()
|
||||||
|
|
||||||
fun ImageRequest.Builder.ehUrl(info: GalleryInfo) = apply {
|
fun ImageRequest.Builder.ehUrl(info: GalleryInfo) = apply {
|
||||||
val key = info.thumbKey!!
|
val key = info.thumbKey!!
|
||||||
data(info.thumbUrl)
|
data(encodeMagicRequestOrUrl(info))
|
||||||
memoryCacheKey(key)
|
memoryCacheKey(key)
|
||||||
diskCacheKey(key)
|
diskCacheKey(key)
|
||||||
|
httpHeaders(header)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun ImageRequest.Builder.ehPreview(preview: GalleryPreview) = apply {
|
fun ImageRequest.Builder.ehPreview(preview: GalleryPreview) = apply {
|
||||||
@@ -25,9 +39,11 @@ fun ImageRequest.Builder.ehPreview(preview: GalleryPreview) = apply {
|
|||||||
memoryCacheKey(preview.imageKey)
|
memoryCacheKey(preview.imageKey)
|
||||||
diskCacheKey(preview.imageKey)
|
diskCacheKey(preview.imageKey)
|
||||||
if (preview is NormalGalleryPreview) size(Size.ORIGINAL)
|
if (preview is NormalGalleryPreview) size(Size.ORIGINAL)
|
||||||
|
httpHeaders(header)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val stubResult = DecodeResult(ColorDrawable(Color.BLACK), false)
|
private val stubImage = ColorDrawable(Color.BLACK).asCoilImage(true)
|
||||||
|
private val stubResult = DecodeResult(stubImage, false)
|
||||||
private val stubFactory = Decoder { stubResult }
|
private val stubFactory = Decoder { stubResult }
|
||||||
|
|
||||||
fun ImageRequest.Builder.justDownload() = apply {
|
fun ImageRequest.Builder.justDownload() = apply {
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package com.hippo.ehviewer.coil
|
||||||
|
|
||||||
|
import coil3.intercept.Interceptor
|
||||||
|
import coil3.intercept.Interceptor.Chain
|
||||||
|
import coil3.request.ErrorResult
|
||||||
|
import coil3.request.ImageResult
|
||||||
|
import coil3.request.allowHardware
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allocating hardware bitmaps may fail on extremely long images.
|
||||||
|
*
|
||||||
|
* Retry with [allowHardware] disabled.
|
||||||
|
*/
|
||||||
|
object HardwareBitmapInterceptor : Interceptor {
|
||||||
|
override suspend fun intercept(chain: Chain): ImageResult {
|
||||||
|
val result = chain.proceed()
|
||||||
|
return if (chain.request.allowHardware && result is ErrorResult && result.throwable is OutOfMemoryError) {
|
||||||
|
val req = chain.request.newBuilder().allowHardware(false).build()
|
||||||
|
chain.withRequest(req).proceed()
|
||||||
|
} else {
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
package com.hippo.ehviewer.coil
|
package com.hippo.ehviewer.coil
|
||||||
|
|
||||||
import coil.intercept.Interceptor
|
import coil3.intercept.Interceptor
|
||||||
import coil.request.ImageResult
|
import coil3.request.ImageResult
|
||||||
import com.hippo.ehviewer.Settings
|
import com.hippo.ehviewer.Settings
|
||||||
import kotlin.coroutines.resume
|
import kotlin.coroutines.resume
|
||||||
import kotlin.coroutines.resumeWithException
|
import kotlin.coroutines.resumeWithException
|
||||||
@@ -41,7 +41,7 @@ object MergeInterceptor : Interceptor {
|
|||||||
|
|
||||||
override suspend fun intercept(chain: Interceptor.Chain): ImageResult {
|
override suspend fun intercept(chain: Interceptor.Chain): ImageResult {
|
||||||
val req = chain.request
|
val req = chain.request
|
||||||
val key = req.memoryCacheKey?.key?.takeIf { it.startsWith("m/") || Settings.preloadThumbAggressively } ?: return withContext(req.interceptorDispatcher) { chain.proceed(req) }
|
val key = req.memoryCacheKey?.takeIf { it.startsWith("m/") || Settings.preloadThumbAggressively } ?: return withContext(req.interceptorDispatcher) { chain.proceed() }
|
||||||
|
|
||||||
suspendCancellableCoroutine { continuation ->
|
suspendCancellableCoroutine { continuation ->
|
||||||
synchronized(pendingContinuationMapLock) {
|
synchronized(pendingContinuationMapLock) {
|
||||||
@@ -62,7 +62,7 @@ object MergeInterceptor : Interceptor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return withContext(req.interceptorDispatcher) { chain.proceed(req) }.apply {
|
return withContext(req.interceptorDispatcher) { chain.proceed() }.apply {
|
||||||
// Wake all pending continuations shared with the same memory key since we have written it to memory cache
|
// Wake all pending continuations shared with the same memory key since we have written it to memory cache
|
||||||
notifyScope.launch {
|
notifyScope.launch {
|
||||||
synchronized(pendingContinuationMapLock) {
|
synchronized(pendingContinuationMapLock) {
|
||||||
|
|||||||
@@ -1,33 +1,11 @@
|
|||||||
package com.hippo.ehviewer.cronet
|
package com.hippo.ehviewer.cronet
|
||||||
|
|
||||||
import com.hippo.ehviewer.EhApplication
|
|
||||||
import io.ktor.utils.io.pool.DirectByteBufferPool
|
import io.ktor.utils.io.pool.DirectByteBufferPool
|
||||||
import io.ktor.utils.io.pool.useInstance
|
import kotlinx.coroutines.Dispatchers
|
||||||
import java.nio.ByteBuffer
|
import kotlinx.coroutines.asExecutor
|
||||||
import java.nio.channels.FileChannel
|
|
||||||
import kotlin.coroutines.resumeWithException
|
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
|
||||||
|
|
||||||
val pool = DirectByteBufferPool(32)
|
val pool = DirectByteBufferPool(32)
|
||||||
|
|
||||||
val cronetHttpClientExecutor = EhApplication.baseOkHttpClient.dispatcher.executorService
|
// Limit thread to 1 since we are async & non-blocking
|
||||||
|
val cronetDispatcher = Dispatchers.Default.limitedParallelism(1)
|
||||||
suspend inline fun CronetRequest.awaitBodyFully(crossinline callback: (ByteBuffer) -> Unit) {
|
val cronetHttpClientExecutor = cronetDispatcher.asExecutor()
|
||||||
return pool.useInstance { buffer ->
|
|
||||||
suspendCancellableCoroutine { cont ->
|
|
||||||
consumer = {
|
|
||||||
callback(it)
|
|
||||||
buffer.clear()
|
|
||||||
request.read(buffer)
|
|
||||||
}
|
|
||||||
onError = { readerCont.resumeWithException(it) }
|
|
||||||
readerCont = cont
|
|
||||||
request.read(buffer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend inline fun CronetRequest.copyToChannel(chan: FileChannel, crossinline listener: ((Int) -> Unit) = {}) = awaitBodyFully {
|
|
||||||
val bytes = chan.write(it)
|
|
||||||
listener(bytes)
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.hippo.ehviewer.cronet
|
||||||
|
|
||||||
|
import com.hippo.ehviewer.Settings
|
||||||
|
import okio.Path.Companion.toOkioPath
|
||||||
|
import org.chromium.net.CronetEngine
|
||||||
|
import splitties.init.appCtx
|
||||||
|
|
||||||
|
val cronetHttpClient: CronetEngine = CronetEngine.Builder(appCtx).apply {
|
||||||
|
enableBrotli(true)
|
||||||
|
setUserAgent(Settings.userAgent)
|
||||||
|
|
||||||
|
// Cache Quic hint only since the real cache mechanism should on Ktor layer
|
||||||
|
val cache = (appCtx.cacheDir.toOkioPath() / "http_cache").toFile().apply { mkdirs() }
|
||||||
|
setStoragePath(cache.absolutePath)
|
||||||
|
enableHttpCache(CronetEngine.Builder.HTTP_CACHE_DISK_NO_HTTP, 4096)
|
||||||
|
|
||||||
|
addQuicHint("e-hentai.org", 443, 443)
|
||||||
|
addQuicHint("forums.e-hentai.org", 443, 443)
|
||||||
|
addQuicHint("exhentai.org", 443, 443)
|
||||||
|
}.build()
|
||||||
@@ -24,16 +24,18 @@ import com.hippo.ehviewer.Settings
|
|||||||
import com.hippo.ehviewer.client.EhCookieStore
|
import com.hippo.ehviewer.client.EhCookieStore
|
||||||
import com.hippo.ehviewer.client.EhEngine
|
import com.hippo.ehviewer.client.EhEngine
|
||||||
import eu.kanade.tachiyomi.util.lang.withIOContext
|
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||||
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
import java.time.Instant
|
|
||||||
import java.time.LocalDateTime
|
|
||||||
import java.time.temporal.ChronoUnit
|
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
import kotlin.time.Duration.Companion.days
|
||||||
|
import kotlinx.datetime.Clock
|
||||||
|
import kotlinx.datetime.LocalDateTime
|
||||||
|
import kotlinx.datetime.LocalTime
|
||||||
|
import kotlinx.datetime.TimeZone
|
||||||
|
import kotlinx.datetime.toInstant
|
||||||
|
import kotlinx.datetime.toLocalDateTime
|
||||||
import splitties.init.appCtx
|
import splitties.init.appCtx
|
||||||
|
|
||||||
private val signedIn
|
|
||||||
get() = EhCookieStore.hasSignedIn()
|
|
||||||
|
|
||||||
private const val CHANNEL_ID = "DailyCheckNotification"
|
private const val CHANNEL_ID = "DailyCheckNotification"
|
||||||
|
|
||||||
class DailyCheckWork(context: Context, workerParams: WorkerParameters) :
|
class DailyCheckWork(context: Context, workerParams: WorkerParameters) :
|
||||||
@@ -46,28 +48,20 @@ class DailyCheckWork(context: Context, workerParams: WorkerParameters) :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val schedHour
|
|
||||||
get() = Settings.requestNewsTimerHour.takeUnless { it == -1 } ?: 0
|
|
||||||
|
|
||||||
val schedMinute
|
|
||||||
get() = Settings.requestNewsTimerMinute.takeUnless { it == -1 } ?: 0
|
|
||||||
|
|
||||||
private val whenToWork
|
|
||||||
get() = LocalDateTime.now()
|
|
||||||
.run { withHour(schedHour) }
|
|
||||||
.run { withMinute(schedMinute) }
|
|
||||||
|
|
||||||
private val initialDelay
|
|
||||||
get() = Duration.between(LocalDateTime.now(), whenToWork)
|
|
||||||
.run { if (isNegative) plusDays(1) else this }
|
|
||||||
|
|
||||||
private fun getDailyCheckWorkRequest(): PeriodicWorkRequest {
|
private fun getDailyCheckWorkRequest(): PeriodicWorkRequest {
|
||||||
|
val now = Clock.System.now()
|
||||||
|
val timeZone = TimeZone.currentSystemDefault()
|
||||||
|
val whenToWork = LocalDateTime(
|
||||||
|
now.toLocalDateTime(timeZone).date,
|
||||||
|
LocalTime.fromSecondOfDay(Settings.requestNewsTime),
|
||||||
|
).toInstant(timeZone)
|
||||||
|
val initialDelay = (whenToWork - now).run { if (isNegative()) plus(1.days) else this }
|
||||||
val constraints = Constraints.Builder()
|
val constraints = Constraints.Builder()
|
||||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||||
.build()
|
.build()
|
||||||
return PeriodicWorkRequestBuilder<DailyCheckWork>(Duration.ofDays(1))
|
return PeriodicWorkRequestBuilder<DailyCheckWork>(Duration.ofDays(1))
|
||||||
.setConstraints(constraints)
|
.setConstraints(constraints)
|
||||||
.setInitialDelay(initialDelay.seconds, TimeUnit.SECONDS)
|
.setInitialDelay(initialDelay.inWholeSeconds, TimeUnit.SECONDS)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,7 +71,7 @@ fun updateDailyCheckWork(context: Context) {
|
|||||||
if (Settings.requestNews) {
|
if (Settings.requestNews) {
|
||||||
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
|
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
|
||||||
WORK_NAME,
|
WORK_NAME,
|
||||||
ExistingPeriodicWorkPolicy.UPDATE,
|
ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE,
|
||||||
getDailyCheckWorkRequest(),
|
getDailyCheckWorkRequest(),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
@@ -86,17 +80,17 @@ fun updateDailyCheckWork(context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val today
|
val today
|
||||||
get() = Instant.now().truncatedTo(ChronoUnit.DAYS).epochSecond
|
get() = (Clock.System.now().epochSeconds / 86400).toInt()
|
||||||
|
|
||||||
suspend fun checkDawn() = runCatching {
|
suspend fun checkDawn() = runCatching {
|
||||||
if (signedIn && Settings.lastDawnDay != today) {
|
if (EhCookieStore.hasSignedIn() && Settings.lastDawnDays != today) {
|
||||||
EhEngine.getNews(true)?.let {
|
EhEngine.getNews(true)?.let {
|
||||||
Settings.lastDawnDay = today
|
Settings.lastDawnDays = today
|
||||||
showEventNotification(it)
|
showEventNotification(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
it.printStackTrace()
|
logcat(WORK_NAME, it)
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("MissingPermission")
|
@SuppressLint("MissingPermission")
|
||||||
@@ -125,6 +119,6 @@ fun showEventNotification(html: String) {
|
|||||||
runCatching {
|
runCatching {
|
||||||
notificationManager.notify(1, msg.build())
|
notificationManager.notify(1, msg.build())
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
it.printStackTrace()
|
logcat(WORK_NAME, it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
package com.hippo.ehviewer.dao
|
|
||||||
|
|
||||||
import androidx.room.ColumnInfo
|
|
||||||
import androidx.room.Entity
|
|
||||||
import androidx.room.PrimaryKey
|
|
||||||
|
|
||||||
@Entity(tableName = "OK_HTTP_3_COOKIE")
|
|
||||||
data class Cookie(
|
|
||||||
@ColumnInfo(name = "NAME")
|
|
||||||
var name: String,
|
|
||||||
@ColumnInfo(name = "VALUE")
|
|
||||||
var value: String,
|
|
||||||
@ColumnInfo(name = "EXPIRES_AT")
|
|
||||||
var expiresAt: Long,
|
|
||||||
@ColumnInfo(name = "DOMAIN")
|
|
||||||
var domain: String,
|
|
||||||
@ColumnInfo(name = "PATH")
|
|
||||||
var path: String,
|
|
||||||
@ColumnInfo(name = "SECURE")
|
|
||||||
var secure: Boolean,
|
|
||||||
@ColumnInfo(name = "HTTP_ONLY")
|
|
||||||
var httpOnly: Boolean,
|
|
||||||
@ColumnInfo(name = "PERSISTENT")
|
|
||||||
var persistent: Boolean,
|
|
||||||
@ColumnInfo(name = "HOST_ONLY")
|
|
||||||
var hostOnly: Boolean,
|
|
||||||
@PrimaryKey
|
|
||||||
@ColumnInfo(name = "_id")
|
|
||||||
var id: Long?,
|
|
||||||
)
|
|
||||||
Loaded 100 of 569 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user