mirror of
https://github.com/InsanusMokrassar/MicroUtils.git
synced 2025-09-17 14:29:24 +00:00
Compare commits
864 Commits
Author | SHA1 | Date | |
---|---|---|---|
7ba6eed453 | |||
beeb6ecc0a | |||
7cdc17a714 | |||
4765a950a9 | |||
65e8137e08 | |||
ae546dd9ad | |||
8110c42be0 | |||
bd2b5ae5fc | |||
2ddfffa6a9 | |||
a4b54e861d | |||
c6785f1a4f | |||
83fe621c56 | |||
b3a93e17eb | |||
546a391af3 | |||
786cf9bd8b | |||
dfd6fe062d | |||
b6ef818613 | |||
b0f9e9c30a | |||
7e5c88ddc3 | |||
9824c3e00f | |||
9171d5ed11 | |||
a1830ebb82 | |||
22d2a3d9bf | |||
c9b97fc965 | |||
51f85becd5 | |||
a8a281cfb4 | |||
ddd1304949 | |||
86d70b6c02 | |||
a22bdb39e7 | |||
7ae4d5ef95 | |||
a2038cbefa | |||
992091eade | |||
e3bfead0c5 | |||
0de96141fd | |||
fa18d15c3c | |||
ea9dbf2371 | |||
d34e3ec7a9 | |||
c8833a36af | |||
a067cb0c0f | |||
999c8327bd | |||
c2ec73c70a | |||
702f782fc1 | |||
25dbcaaf83 | |||
b00d454a24 | |||
dbc921d56d | |||
438fefa7a3 | |||
5d74eac814 | |||
9fb62e1e25 | |||
3e366ea73b | |||
0ff895bffa | |||
c5bb120280 | |||
4b26a92b37 | |||
0a8453b4d2 | |||
c9872a61b6 | |||
149a1aa278 | |||
13d0e1b682 | |||
6f9be2a9f8 | |||
93ba98d993 | |||
940ee3df06 | |||
2e7bab10fd | |||
|
3ed70a37ea | ||
fe8f80b9d9 | |||
d81fb32fb9 | |||
2877b5532c | |||
b938b21395 | |||
58836359cc | |||
5edb0e1331 | |||
0f0d0b5d58 | |||
46c1887cbe | |||
5f231c2212 | |||
4e97ce86aa | |||
315a7cb29e | |||
aa7cc503f2 | |||
4bbe7e5a80 | |||
d9c05f38d2 | |||
cd0c4c9650 | |||
fc3407f104 | |||
3a5544206b | |||
e17e2f7fb8 | |||
|
d32c95f143 | ||
6d8a8ab018 | |||
a7dce8fa78 | |||
ca73ff8e19 | |||
d01ad10d7d | |||
81041ee43c | |||
|
6e004c2ae4 | ||
0e2fac5b22 | |||
269da7f155 | |||
3cb6b73ee0 | |||
a938ee1efb | |||
6ea5e2e5a6 | |||
617dfb54e0 | |||
d23e005985 | |||
e5207f5bc5 | |||
c96cea8db0 | |||
0a8e71d76a | |||
cf1fd32b08 | |||
cc4224ea1f | |||
f4c148bc58 | |||
022297ad3f | |||
5180d6fc3e | |||
eeebbff70d | |||
afc6aeea15 | |||
486515eddd | |||
0e21699cd1 | |||
f1678ef7cf | |||
cea65fc76e | |||
c9e320b72a | |||
555956087d | |||
b3f468f901 | |||
f5f7511781 | |||
4be1d93f60 | |||
7d684608ef | |||
2c7fd320eb | |||
88ee82e1c6 | |||
d6402c624e | |||
8b9c93bc10 | |||
4f5e261d01 | |||
cf455aebe6 | |||
1380d5f8e1 | |||
5f65260bfe | |||
11f0dcfc01 | |||
555b7886a4 | |||
3707a6c0ce | |||
4c8d92b4c3 | |||
8bbd33c896 | |||
ac33a3580f | |||
a64a32fbe6 | |||
9493e97a38 | |||
88bd770260 | |||
a7bd33b7bf | |||
73c724a2e5 | |||
d8cf3c6726 | |||
15dee238b5 | |||
c70626734e | |||
5a765ea1bc | |||
8215f9d2c6 | |||
d2e6d2ec80 | |||
3718181e8f | |||
0d825cf424 | |||
28a804d988 | |||
9e4bb9d678 | |||
9c40d7da3d | |||
2b76ad0aa9 | |||
e4b619e050 | |||
36c09feaf2 | |||
2d68321503 | |||
85455ab21c | |||
18d63eb980 | |||
2e429e9704 | |||
f4af28059b | |||
c1476bd075 | |||
16c720fddd | |||
8b4b4a5eca | |||
32e6e5b7e2 | |||
a9f7fd8e32 | |||
95be1a26f2 | |||
ef9b31aee0 | |||
df3c01ff0a | |||
4704c5a33d | |||
225c06550a | |||
f0987614c6 | |||
269c2876f3 | |||
168d6acf7c | |||
a5f718e257 | |||
4f68459582 | |||
442db122cf | |||
580d757be2 | |||
47b0f6d2d8 | |||
3f6f6ebc2b | |||
2645ea29d6 | |||
79f2041565 | |||
4a7567f288 | |||
8a890ed6ed | |||
3d90df6897 | |||
681c13144a | |||
b64f2e6d32 | |||
428eabb1bd | |||
2162e83bce | |||
6142022283 | |||
e6d9c8250f | |||
46178e723b | |||
605f55acd2 | |||
0f8b69aa60 | |||
551d8ec480 | |||
fc48446ec4 | |||
3644b83ac6 | |||
cd73791b6f | |||
03de71df2e | |||
83d5d3faf4 | |||
0c8bec4c89 | |||
7fc93817c1 | |||
d0a00031a1 | |||
6ebc5aa0c2 | |||
8a6b4bb49e | |||
20799b9a3e | |||
ec3afc615c | |||
da692ccfc3 | |||
53b89f3a18 | |||
58cded28d3 | |||
592c5f3732 | |||
f44a78a5f5 | |||
e0bdd5dfdc | |||
99c0f06b72 | |||
66fc6df3d7 | |||
a36425a905 | |||
d920fee6d4 | |||
23590be5de | |||
94acc3c93b | |||
5616326a3b | |||
7601860c5c | |||
8b43d785cc | |||
b62d3a0b7d | |||
fad73c7213 | |||
2403c7c2b0 | |||
fa090bf920 | |||
a83ee86340 | |||
204955bcce | |||
ee56e9543a | |||
96fdff6ffd | |||
58b007cbb3 | |||
4f0c139889 | |||
c584c24fce | |||
85e5cee154 | |||
5af91981f1 | |||
2fe4f08059 | |||
83796f345a | |||
c1e21364a6 | |||
067d9d0d3b | |||
03f527d83e | |||
ced05a4586 | |||
43fe06206a | |||
023657558e | |||
9b0b726c80 | |||
4ee67321c4 | |||
59f1f2e59b | |||
0766d48b7c | |||
e18903b9e9 | |||
d0eecdead2 | |||
cc4a83a033 | |||
1cf911bbde | |||
36d73d5023 | |||
c395242e3e | |||
cd9cd7cc5d | |||
acbb8a0c07 | |||
b9d8528599 | |||
4971326eca | |||
09d1047260 | |||
02dbd493c2 | |||
b17931e7bd | |||
2a4570eafc | |||
c9514d3a6d | |||
072805efc7 | |||
369ff26627 | |||
c5abbbbd2d | |||
d974639f1e | |||
26efde316b | |||
fafe50f80a | |||
41504469db | |||
03b3ddd98b | |||
89d919f2be | |||
b53cfd5504 | |||
31022733ac | |||
f9a8c39879 | |||
a812c2dd2f | |||
217e977f0d | |||
04c301d1ac | |||
7f0c425389 | |||
1ede1c423b | |||
7cb064896a | |||
0c5e2862ca | |||
30d4507f54 | |||
b6c6d89455 | |||
9d218ee534 | |||
11116d8cab | |||
58d754bbde | |||
8f25c123dd | |||
76e214fc08 | |||
2b5380f8d6 | |||
844a129094 | |||
a3090cca9d | |||
b7b5159e9c | |||
0f8bc2c950 | |||
69f5c49f45 | |||
9b308e6fb8 | |||
3e3f91128b | |||
0d1aae0ef7 | |||
4d022f0480 | |||
153e20d00e | |||
a9a8171dd6 | |||
bf5c3b59b2 | |||
607c432bdb | |||
ae5c010770 | |||
d5e432437f | |||
9f99ebce01 | |||
64ee899b84 | |||
e0e0c1658b | |||
2c586f667c | |||
64164ef6c1 | |||
22343c0731 | |||
f4ec1a4c60 | |||
c1c33cceb1 | |||
a3e975b2ba | |||
06e705a687 | |||
d56eb6c867 | |||
9cbca864e3 | |||
abb4378694 | |||
0eb698d9a4 | |||
15ea9f2093 | |||
d47aca0923 | |||
1ac50e9959 | |||
6adfbe3a96 | |||
59f36e62e9 | |||
54af116009 | |||
38fbec8e3b | |||
babbfc55e4 | |||
2511e18d69 | |||
29658c70a0 | |||
96311ee43d | |||
bd33b09052 | |||
8055003b47 | |||
1257492f85 | |||
1107b7f4ef | |||
a1a1171240 | |||
46c02e5df1 | |||
2e9efc57de | |||
acecadef17 | |||
19394b5e69 | |||
de999e197f | |||
9d95687d3c | |||
aa9dfb4ab8 | |||
9c5b44efb3 | |||
ac587a67e6 | |||
59428140a8 | |||
60bdb59d71 | |||
be52871de8 | |||
b7934cf357 | |||
dbfbeef90a | |||
00943c9cdf | |||
8745c6a16a | |||
433ba4b58f | |||
d40376e524 | |||
a2982f88f5 | |||
1642f7abd9 | |||
a10d2184ff | |||
522435f096 | |||
79b30290c0 | |||
f8b8626859 | |||
b061b85a08 | |||
3870db1c88 | |||
1be1070eb4 | |||
2696e663cf | |||
1e1f7df86d | |||
1d8ded8fd3 | |||
197825123a | |||
422b2e6db1 | |||
1973e0b5bf | |||
8258cf93a9 | |||
1d49bd5947 | |||
44317d1519 | |||
48e08fcc69 | |||
1a3ce6e623 | |||
fb122f3e70 | |||
7cca6039cc | |||
118e3dba39 | |||
87070710fa | |||
38499c3d4a | |||
5d754d968b | |||
d543d436bc | |||
12b54f99af | |||
2a95d7e643 | |||
e3d3cacfa4 | |||
4b13491a0e | |||
85d516d1e9 | |||
ac58b6a7e3 | |||
2cc6126765 | |||
f94b085850 | |||
c9822a491b | |||
23b2d60295 | |||
f4bc9eed39 | |||
e310f188b0 | |||
6c1571188c | |||
945d2fa284 | |||
020095f1ff | |||
b165a76e62 | |||
03c8830672 | |||
38448da89b | |||
2ade5aff91 | |||
29bf6e80ec | |||
027e927e1b | |||
fa061f88e2 | |||
1d01b65b5f | |||
2c2b364167 | |||
7c5fc9bf7c | |||
193d22ff20 | |||
0f3b553ba2 | |||
4eb1013446 | |||
be1b2563ec | |||
1c9c2f1e70 | |||
52dfded741 | |||
0a115e5cf4 | |||
c349af999b | |||
9a5709a34d | |||
28d311160b | |||
1ca7081a40 | |||
5ce8ebe82c | |||
a7a88b29b9 | |||
cd7b982385 | |||
ee59100075 | |||
f808ac58ef | |||
f33ada5396 | |||
984d781f2f | |||
dd33e1e8bc | |||
2950de29e5 | |||
50a8799f9d | |||
bdb0ce6fc7 | |||
18ec2bca96 | |||
937ef48794 | |||
6331f13e9a | |||
5213a2ff8e | |||
087d7452fd | |||
703094c924 | |||
eea645c865 | |||
324832a189 | |||
d55d735c51 | |||
e3ff1b9609 | |||
70c31966ca | |||
0e4188882f | |||
6bf0ce92ba | |||
d51bdc5086 | |||
f04f262cee | |||
0f172055ef | |||
e5dd4363f1 | |||
a3a48bbaac | |||
5e716fb9a8 | |||
11a36153cc | |||
8bee354f04 | |||
f7dd2b5ce7 | |||
8ca10c00bb | |||
905c7e8eda | |||
d4c5e849bf | |||
8250a2a021 | |||
01b3df7b8c | |||
daa6e4aff5 | |||
23bcb26a58 | |||
e55f60c30b | |||
0d0c16e16d | |||
540d5cce7c | |||
a548b00979 | |||
4b7ca6d565 | |||
0473fa238c | |||
cfc7119697 | |||
22a6520d3e | |||
fb25e91191 | |||
c116b270b6 | |||
aa2d598689 | |||
5ef3bb746b | |||
037616e271 | |||
abbea906f1 | |||
9132e216c9 | |||
12a7e3c4af | |||
b40c093917 | |||
7ac12455c8 | |||
5043eec7a2 | |||
cf31f53e01 | |||
cd22d76fa7 | |||
c8759843f7 | |||
781bbcc012 | |||
6da29c0686 | |||
fcdb6fc45a | |||
e785a99bd7 | |||
121e513fdd | |||
9cf01ab54f | |||
c655107681 | |||
91a5af6a9a | |||
86e74c0a6f | |||
d8f01f21a0 | |||
9fb8626d8c | |||
1c52e04cdb | |||
798128256e | |||
72c2df47fd | |||
c9c6d4c0c1 | |||
2f4f9f3003 | |||
22e8f8e5d6 | |||
04cf8c3d9a | |||
52198be543 | |||
80fd5a489b | |||
b187043ee1 | |||
8965752055 | |||
b796620267 | |||
62df81bb4e | |||
0a8e0f6178 | |||
f705020aaa | |||
78bd3b9853 | |||
992b283597 | |||
8fc1ff1d59 | |||
3bbde61f39 | |||
8d955c4b9d | |||
18593c530b | |||
78903cd4eb | |||
eaa143f7d7 | |||
bcb0e42fa2 | |||
8eed435302 | |||
0e4a63057f | |||
1af5faa440 | |||
7412217b0c | |||
3e749d75b7 | |||
846b5c87c9 | |||
7b5e84f80b | |||
27822a8a66 | |||
36f4e7ec37 | |||
187e84ad65 | |||
195fe221c4 | |||
6f9c19bbf6 | |||
65bdab4f7e | |||
c4d5fcfc22 | |||
43232afa62 | |||
18908a01d7 | |||
b22fbfb3bc | |||
57aaea88b6 | |||
140949c5ea | |||
639241e0d3 | |||
e0eb42bc2d | |||
7990b21cc5 | |||
2ba5c97709 | |||
33b7c85fc2 | |||
7f6c02ffdf | |||
f368616e6f | |||
dd632f4203 | |||
a8f3ae501b | |||
ea76963ac2 | |||
99dfa97958 | |||
f68270a5b3 | |||
542ed81034 | |||
404a11f5e7 | |||
411221070e | |||
25d35d0c76 | |||
c72904d61c | |||
b94c9acd26 | |||
bdb4988569 | |||
1dafd1352a | |||
31fbd3cad7 | |||
848bc5ec10 | |||
4d0ad826a0 | |||
6955820fcc | |||
ef530624b9 | |||
9b9e7dd88f | |||
a13cc9e961 | |||
0d2b923378 | |||
fba84c8ac8 | |||
db10fe1b2c | |||
175dd980f8 | |||
8364020671 | |||
eba44cd394 | |||
b3bac8015a | |||
0b48afd251 | |||
19857930a4 | |||
d0dbe3ed2f | |||
8b7e78b63a | |||
92a4ecb523 | |||
6a5ad4d728 | |||
be4aa8daac | |||
b5eac37782 | |||
b1ad3c5a39 | |||
ba16bad029 | |||
ca8ae4cd72 | |||
53d35d74b3 | |||
49c139e235 | |||
caf9c821f3 | |||
ca4c6db96f | |||
6b2298c752 | |||
a1bf43def9 | |||
15e9254e00 | |||
afe5a72c6f | |||
750a8b9ecf | |||
27fc3f93e0 | |||
8166d4b99b | |||
b61d2ae2eb | |||
4790fe0aea | |||
bc37b11cee | |||
223fed910f | |||
b85ab7b061 | |||
888dc299c9 | |||
e113dc28ed | |||
31e55d2307 | |||
e90645f248 | |||
4bb7ba2571 | |||
8d31c25bf8 | |||
c7ee1c28b2 | |||
99b09c8b28 | |||
a328c4425a | |||
c0f61ca896 | |||
86e70c0961 | |||
d87a3a039f | |||
6279a2c40a | |||
f377ebea88 | |||
7373fef964 | |||
41ef86dbda | |||
7bc2b2336d | |||
0a615e6d78 | |||
8f928e16e1 | |||
72bc8da1e7 | |||
51e349a5db | |||
3cbb19ba2c | |||
ae3a5bf45d | |||
9c667f4b78 | |||
21195e1bcb | |||
03117ac565 | |||
d13fbdf176 | |||
7cecc0e0b6 | |||
203e781f5d | |||
3eb6cd77cd | |||
51855b2405 | |||
00cc26d874 | |||
02520636ad | |||
76813fae8e | |||
4693483c2b | |||
6dbd12df59 | |||
9e84dc5031 | |||
8f790360bc | |||
808375cea6 | |||
d9c15db9de | |||
cf2be8ed43 | |||
97339c9b1d | |||
eb35903de9 | |||
43e88e00da | |||
1e51f9d77b | |||
dad738357c | |||
cb828ab3f2 | |||
aefaf4a8cc | |||
b29f37a251 | |||
88854020ac | |||
14ebe01fc6 | |||
771aed0f0f | |||
16c57fcd6a | |||
75397a7ccb | |||
b05404f828 | |||
edea942874 | |||
8a8f568b9a | |||
4dc8d30c52 | |||
cb4e08e823 | |||
c443bf4fa0 | |||
a6982de822 | |||
4f1a663e75 | |||
dab262d626 | |||
87a3f61ca6 | |||
506e937a68 | |||
5a037c76dd | |||
313f622f7e | |||
6cba1fe1a2 | |||
fd2d0e80b7 | |||
96ab2e8aca | |||
0202988cae | |||
d619d59947 | |||
85b3e48d18 | |||
7a9b7d98a1 | |||
b212acfcaf | |||
3a45e5dc70 | |||
73190518d5 | |||
03f78180dc | |||
1c0b8cf842 | |||
a1624ea2a9 | |||
23a050cf1e | |||
916f2f96f4 | |||
00cc214754 | |||
b2e38f72b9 | |||
e7107d238d | |||
ed9ebdbd1a | |||
e80676d3d2 | |||
02d02fa8f2 | |||
bd783fb74f | |||
50386adf70 | |||
f4ee6c2890 | |||
d45aef9fe5 | |||
a56cd3dddd | |||
419e7070ee | |||
612cf40b5f | |||
8b39882e83 | |||
e639ae172b | |||
d0446850ae | |||
c48465b90b | |||
f419fd03d2 | |||
494812a660 | |||
eb78f21eec | |||
4bda70268b | |||
f037ce4371 | |||
3d2196e35d | |||
a74f061b02 | |||
11ade14676 | |||
eb562d8784 | |||
1ee5b4bfd4 | |||
d97892080b | |||
6f37125724 | |||
ed1baaade7 | |||
bb9669f8fd | |||
bdac715d48 | |||
acf4971298 | |||
249bc83a8c | |||
0fbb92f03f | |||
ca27cb3f82 | |||
3a5771a0cc | |||
527a2a91ac | |||
6763e5c4c6 | |||
06918d8310 | |||
89ccaa1b57 | |||
5d0bdb9bcf | |||
31fdcf74a5 | |||
afca09cc1d | |||
531d89d9db | |||
6bbbea0bc3 | |||
e337cd98c8 | |||
bcbab3b380 | |||
fb63de7568 | |||
aa45a4ab13 | |||
2af7e2f681 | |||
34fd9edce0 | |||
2a4cb8c5f9 | |||
50ea40bc3a | |||
a77654052d | |||
88aafce552 | |||
4e95d6bfff | |||
38d0e34fb5 | |||
8fbc6b9041 | |||
e8219d6cf4 | |||
6c20fc4ca6 | |||
85cd975492 | |||
1171a717fe | |||
bbe5320312 | |||
00acb9fddd | |||
de3d14dc41 | |||
67ff9cc9b3 | |||
af132103a0 | |||
3b1124a804 | |||
f226c2dfd6 | |||
69d6e63846 | |||
02c3d397ad | |||
67a1050646 | |||
8cd0775a6c | |||
162294d6c6 | |||
c4dd19dd00 | |||
d2314422f1 | |||
6fedd6f859 | |||
e52b59665f | |||
cda9d09689 | |||
c9237b3f00 | |||
18bba66c4a | |||
63418c4a8a | |||
2e66c6f4e3 | |||
e9c5df4c13 | |||
bc7789ad2c | |||
e3da761249 | |||
4082f65afa | |||
5d1cab075d | |||
bcf67f7e59 | |||
7d3b1f8e75 | |||
119a0588cc | |||
fab789d9c0 | |||
ceba81c08f | |||
a061af0558 | |||
c7a53846ad | |||
a683cccf0c | |||
50d41e35c1 | |||
aa0e831cea | |||
44e26ccb4f | |||
2a783f6e2b | |||
6058d6a724 | |||
2e9c7eb5fa | |||
e75465ad10 | |||
de01ad54e9 | |||
eeea7ddbe3 | |||
e0b18bec05 | |||
410e89bba9 | |||
9ef19dc42b | |||
0337d1b82d | |||
f5bd4c5ccb | |||
630f9bc0d4 | |||
18b4ffece1 | |||
f64e1effa3 | |||
847fcbb488 | |||
88002ec8e7 | |||
7f8db6a29d | |||
b183b82443 | |||
5dad27de72 | |||
6b66084d0e | |||
50b56a7c39 | |||
7ab7d14471 | |||
bdcc179b7b | |||
55ffd4b46f | |||
7fc5ee70e1 | |||
a24a335743 | |||
ef9af71960 | |||
925702d315 | |||
d50dffec8c | |||
cef2081a13 | |||
06c8bde7c9 | |||
c9bbfa3820 | |||
eed7cfdc42 | |||
bd9b0d16ab | |||
ea6c33b497 | |||
dc80ade2fb | |||
f6a06ee8ea | |||
2644f27975 | |||
3dc68a7b8b | |||
97fc1d6239 | |||
662f4d22a3 | |||
b70aa12be9 | |||
71f12f5f19 | |||
e10504eeeb | |||
2dea9f3bc0 | |||
35c9dda5bc | |||
e831f3949a | |||
b0b39cc693 | |||
fc03be3f73 | |||
b61f6b81f1 | |||
f5bc1c1fce | |||
a729f9568c | |||
5749e00377 | |||
ef73c24a0c | |||
94717ee351 | |||
9a18ded65b | |||
b23220f491 | |||
6e6bb03246 | |||
1ae6bae3b8 | |||
1239ca3256 | |||
57b7797ea4 | |||
5ee5bfd1d5 | |||
7229a3e198 | |||
bee083582f | |||
9d7f99f286 | |||
6ef403853c | |||
6ae7ccb9a1 | |||
dafc50c463 | |||
e89e2c931d | |||
43a67b99e4 | |||
46c48f4f31 | |||
bf0fe85aa6 | |||
42c5bd3a7f | |||
d170e86c8a | |||
e3078169b1 | |||
a33ad123f6 | |||
7e14fa2f5c | |||
ba698b41e1 | |||
e76215987e | |||
d1a247af8c | |||
2b7e9534f3 | |||
38521558a1 | |||
100f3d214b | |||
1309867611 | |||
611f64f2e1 | |||
f118ebce6e | |||
59fc90e556 | |||
fb9e4d57fb | |||
960c38b696 | |||
39895e58a6 | |||
b420d85be5 | |||
19ea2f340a | |||
11b0d059bf | |||
c8a25ce544 | |||
509583ea2e | |||
1c86f3f4bf | |||
6d999be590 | |||
e715772dbf | |||
63eb7b7ea8 | |||
b07683b815 |
16
.github/workflows/build.yml
vendored
16
.github/workflows/build.yml
vendored
@@ -1,4 +1,5 @@
|
||||
name: Regular build
|
||||
|
||||
name: Build
|
||||
on: [push]
|
||||
jobs:
|
||||
build:
|
||||
@@ -7,6 +8,17 @@ jobs:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-java@v1
|
||||
with:
|
||||
java-version: 1.8
|
||||
java-version: 11
|
||||
- name: Rewrite version
|
||||
run: |
|
||||
branch="`echo "${{ github.ref }}" | grep -o "[^/]*$"`"
|
||||
cat gradle.properties | sed -e "s/^version=\([0-9\.]*\)/version=\1-branch_$branch-build${{ github.run_number }}/" > gradle.properties.tmp
|
||||
rm gradle.properties
|
||||
mv gradle.properties.tmp gradle.properties
|
||||
- name: Build
|
||||
run: ./gradlew build
|
||||
- name: Publish
|
||||
continue-on-error: true
|
||||
run: ./gradlew publishAllPublicationsToGiteaRepository
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
|
4
.github/workflows/dokka_push.yml
vendored
4
.github/workflows/dokka_push.yml
vendored
@@ -10,9 +10,9 @@ jobs:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-java@v1
|
||||
with:
|
||||
java-version: 1.8
|
||||
java-version: 11
|
||||
- name: Build
|
||||
run: ./gradlew dokkaHtml
|
||||
run: ./gradlew build && ./gradlew dokkaHtml
|
||||
- name: Publish KDocs
|
||||
uses: peaceiris/actions-gh-pages@v3
|
||||
with:
|
||||
|
24
.github/workflows/packages_push.yml
vendored
24
.github/workflows/packages_push.yml
vendored
@@ -1,24 +0,0 @@
|
||||
|
||||
name: Publish package to GitHub Packages
|
||||
on: [push]
|
||||
jobs:
|
||||
publishing:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-java@v1
|
||||
with:
|
||||
java-version: 1.8
|
||||
- name: Rewrite version
|
||||
run: |
|
||||
branch="`echo "${{ github.ref }}" | grep -o "[^/]*$"`"
|
||||
cat gradle.properties | sed -e "s/^version=\([0-9\.]*\)/version=\1-branch_$branch-build${{ github.run_number }}/" > gradle.properties.tmp
|
||||
rm gradle.properties
|
||||
mv gradle.properties.tmp gradle.properties
|
||||
- name: Build
|
||||
run: ./gradlew build
|
||||
- name: Publish
|
||||
run: ./gradlew --no-parallel publishAllPublicationsToGithubPackagesRepository -x signJsPublication -x signJvmPublication -x signKotlinMultiplatformPublication -x signAndroidDebugPublication -x signAndroidReleasePublication -x signKotlinMultiplatformPublication
|
||||
env:
|
||||
GITHUBPACKAGES_USER: ${{ github.actor }}
|
||||
GITHUBPACKAGES_PASSWORD: ${{ secrets.GITHUB_TOKEN }}
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -11,5 +11,6 @@ out/
|
||||
|
||||
secret.gradle
|
||||
local.properties
|
||||
kotlin-js-store
|
||||
|
||||
publishing.sh
|
||||
|
27
.travis.yml
27
.travis.yml
@@ -1,27 +0,0 @@
|
||||
language: android
|
||||
install: true
|
||||
|
||||
os: linux
|
||||
dist: trusty
|
||||
jdk: oraclejdk8
|
||||
|
||||
android:
|
||||
components:
|
||||
- tools
|
||||
- platform-tools
|
||||
- build-tools-30.0.2
|
||||
- android-30
|
||||
- add-on
|
||||
- extra
|
||||
|
||||
before_script:
|
||||
- yes | /usr/local/android-sdk/tools/bin/sdkmanager "build-tools;30.0.2"
|
||||
- yes | /usr/local/android-sdk/tools/bin/sdkmanager "platforms;android-30"
|
||||
|
||||
jobs:
|
||||
include:
|
||||
- stage: build
|
||||
script: ./gradlew build -s -x jvmTest -x jsIrTest -x jsIrBrowserTest -x jsIrNodeTest -x jsLegacyTest -x jsLegacyBrowserTest -x jsLegacyNodeTest
|
||||
# Tests are temporarily disabled on public travis due to the problems of launching
|
||||
# - state: test
|
||||
# script: ./gradlew allTests
|
1209
CHANGELOG.md
1209
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
@@ -10,7 +10,7 @@ kotlin {
|
||||
sourceSets {
|
||||
androidMain {
|
||||
dependencies {
|
||||
api "androidx.appcompat:appcompat-resources:$appcompat_version"
|
||||
api libs.android.appCompat.resources
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -10,12 +10,13 @@ kotlin {
|
||||
sourceSets {
|
||||
commonMain {
|
||||
dependencies {
|
||||
api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version"
|
||||
api libs.kt.coroutines
|
||||
api project(":micro_utils.common")
|
||||
}
|
||||
}
|
||||
androidMain {
|
||||
dependencies {
|
||||
api "androidx.recyclerview:recyclerview:$androidx_recycler_version"
|
||||
api libs.android.recyclerView
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -3,11 +3,19 @@ package dev.inmo.micro_utils.android.recyclerview
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
|
||||
abstract class RecyclerViewAdapter<T>: RecyclerView.Adapter<AbstractViewHolder<T>>() {
|
||||
protected abstract val data: List<T>
|
||||
|
||||
private val _dataCountState by lazy {
|
||||
MutableStateFlow<Int>(data.size)
|
||||
}
|
||||
val dataCountState: StateFlow<Int> by lazy {
|
||||
_dataCountState.asStateFlow()
|
||||
}
|
||||
|
||||
var emptyView: View? = null
|
||||
set(value) {
|
||||
field = value
|
||||
@@ -19,31 +27,37 @@ abstract class RecyclerViewAdapter<T>: RecyclerView.Adapter<AbstractViewHolder<T
|
||||
object : RecyclerView.AdapterDataObserver() {
|
||||
override fun onItemRangeChanged(positionStart: Int, itemCount: Int) {
|
||||
super.onItemRangeChanged(positionStart, itemCount)
|
||||
_dataCountState.value = data.size
|
||||
checkEmpty()
|
||||
}
|
||||
|
||||
override fun onItemRangeChanged(positionStart: Int, itemCount: Int, payload: Any?) {
|
||||
super.onItemRangeChanged(positionStart, itemCount, payload)
|
||||
_dataCountState.value = data.size
|
||||
checkEmpty()
|
||||
}
|
||||
|
||||
override fun onChanged() {
|
||||
super.onChanged()
|
||||
_dataCountState.value = data.size
|
||||
checkEmpty()
|
||||
}
|
||||
|
||||
override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) {
|
||||
super.onItemRangeRemoved(positionStart, itemCount)
|
||||
_dataCountState.value = data.size
|
||||
checkEmpty()
|
||||
}
|
||||
|
||||
override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) {
|
||||
super.onItemRangeMoved(fromPosition, toPosition, itemCount)
|
||||
_dataCountState.value = data.size
|
||||
checkEmpty()
|
||||
}
|
||||
|
||||
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
|
||||
super.onItemRangeInserted(positionStart, itemCount)
|
||||
_dataCountState.value = data.size
|
||||
checkEmpty()
|
||||
}
|
||||
}
|
||||
@@ -59,7 +73,7 @@ abstract class RecyclerViewAdapter<T>: RecyclerView.Adapter<AbstractViewHolder<T
|
||||
|
||||
private fun checkEmpty() {
|
||||
emptyView ?. let {
|
||||
if (data.isEmpty()) {
|
||||
if (dataCountState.value == 0) {
|
||||
it.visibility = View.VISIBLE
|
||||
} else {
|
||||
it.visibility = View.GONE
|
||||
|
@@ -0,0 +1,50 @@
|
||||
package dev.inmo.micro_utils.android.recyclerview
|
||||
|
||||
import dev.inmo.micro_utils.common.Diff
|
||||
import dev.inmo.micro_utils.common.PreviewFeature
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
@PreviewFeature("This feature in preview state and may contains different bugs. " +
|
||||
"Besides, this feature can be changed in future in non-compatible way")
|
||||
abstract class StateFlowBasedRecyclerViewAdapter<T>(
|
||||
listeningScope: CoroutineScope,
|
||||
dataState: StateFlow<List<T>>
|
||||
) : RecyclerViewAdapter<T>() {
|
||||
override var data: List<T> = emptyList()
|
||||
|
||||
init {
|
||||
dataState.onEach {
|
||||
try {
|
||||
val diffForRemoves = Diff(data, it)
|
||||
val removedIndexes = diffForRemoves.removed.map { it.index }
|
||||
val leftRemove = removedIndexes.toMutableList()
|
||||
data = data.filterIndexed { i, _ ->
|
||||
if (i in leftRemove) {
|
||||
leftRemove.remove(i)
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
removedIndexes.sortedDescending().forEach {
|
||||
notifyItemRemoved(it)
|
||||
}
|
||||
}
|
||||
val diffAddsAndReplaces = Diff(data, it)
|
||||
data = it
|
||||
withContext(Dispatchers.Main) {
|
||||
diffAddsAndReplaces.replaced.forEach { (from, to) ->
|
||||
notifyItemMoved(from.index, to.index)
|
||||
}
|
||||
diffAddsAndReplaces.added.forEach {
|
||||
notifyItemInserted(it.index)
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
// currently do nothing
|
||||
}
|
||||
}.launchIn(listeningScope)
|
||||
}
|
||||
}
|
23
build.gradle
23
build.gradle
@@ -1,6 +1,5 @@
|
||||
buildscript {
|
||||
repositories {
|
||||
jcenter()
|
||||
google()
|
||||
mavenCentral()
|
||||
mavenLocal()
|
||||
@@ -8,22 +7,27 @@ buildscript {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:4.1.3'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
|
||||
classpath "com.getkeepsafe.dexcount:dexcount-gradle-plugin:$dexcount_version"
|
||||
classpath "com.github.breadmoirai:github-release:$github_release_plugin_version"
|
||||
classpath "org.jetbrains.dokka:dokka-gradle-plugin:$dokka_version"
|
||||
classpath libs.buildscript.kt.gradle
|
||||
classpath libs.buildscript.kt.serialization
|
||||
classpath libs.buildscript.kt.ksp
|
||||
classpath libs.buildscript.jb.dokka
|
||||
classpath libs.buildscript.gh.release
|
||||
classpath libs.buildscript.android.gradle
|
||||
classpath libs.buildscript.android.dexcount
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.versions)
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
mavenLocal()
|
||||
jcenter()
|
||||
mavenCentral()
|
||||
google()
|
||||
maven { url "https://kotlin.bintray.com/kotlinx" }
|
||||
maven { url "https://maven.pkg.jetbrains.space/public/p/compose/dev" }
|
||||
maven { url "https://git.inmo.dev/api/packages/InsanusMokrassar/maven" }
|
||||
}
|
||||
|
||||
// temporal crutch until legacy tests will be stabled or legacy target will be removed
|
||||
@@ -38,3 +42,4 @@ allprojects {
|
||||
|
||||
apply from: "./extensions.gradle"
|
||||
apply from: "./github_release.gradle"
|
||||
apply from: "./versions_plugin_setup.gradle"
|
||||
|
@@ -5,3 +5,31 @@ plugins {
|
||||
}
|
||||
|
||||
apply from: "$mppProjectWithSerializationPresetPath"
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
jvmMain {
|
||||
dependencies {
|
||||
api project(":micro_utils.coroutines")
|
||||
}
|
||||
}
|
||||
androidMain {
|
||||
dependencies {
|
||||
api project(":micro_utils.coroutines")
|
||||
api libs.android.fragment
|
||||
}
|
||||
dependsOn jvmMain
|
||||
}
|
||||
|
||||
linuxX64Main {
|
||||
dependencies {
|
||||
api libs.okio
|
||||
}
|
||||
}
|
||||
mingwX64Main {
|
||||
dependencies {
|
||||
api libs.okio
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
18
common/compose/build.gradle
Normal file
18
common/compose/build.gradle
Normal file
@@ -0,0 +1,18 @@
|
||||
plugins {
|
||||
id "org.jetbrains.kotlin.multiplatform"
|
||||
id "org.jetbrains.kotlin.plugin.serialization"
|
||||
id "com.android.library"
|
||||
alias(libs.plugins.jb.compose)
|
||||
}
|
||||
|
||||
apply from: "$mppProjectWithSerializationAndComposePresetPath"
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
commonMain {
|
||||
dependencies {
|
||||
api project(":micro_utils.common")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,16 @@
|
||||
package dev.inmo.micro_utils.common.compose
|
||||
|
||||
import androidx.compose.runtime.DisposableEffectResult
|
||||
|
||||
class DefaultDisposableEffectResult(
|
||||
private val onDispose: () -> Unit
|
||||
) : DisposableEffectResult {
|
||||
override fun dispose() {
|
||||
onDispose()
|
||||
}
|
||||
|
||||
companion object {
|
||||
val DoNothing = DefaultDisposableEffectResult {}
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,10 @@
|
||||
package dev.inmo.micro_utils.common.compose
|
||||
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
|
||||
/**
|
||||
* Converts current [MutableState] to immutable [State] using [derivedStateOf]
|
||||
*/
|
||||
fun <T> MutableState<T>.asState(): State<T> = derivedStateOf { this.value }
|
@@ -0,0 +1,9 @@
|
||||
package dev.inmo.micro_utils.common.compose
|
||||
|
||||
import androidx.compose.runtime.Composition
|
||||
import dev.inmo.micro_utils.common.onRemoved
|
||||
import org.w3c.dom.Element
|
||||
|
||||
fun Composition.linkWithElement(element: Element) {
|
||||
element.onRemoved { dispose() }
|
||||
}
|
@@ -0,0 +1,10 @@
|
||||
package dev.inmo.micro_utils.common.compose
|
||||
|
||||
import org.jetbrains.compose.web.attributes.ATarget
|
||||
|
||||
fun openLink(link: String, mode: ATarget = ATarget.Blank, features: String = "") = dev.inmo.micro_utils.common.openLink(
|
||||
link,
|
||||
mode.targetStr,
|
||||
features
|
||||
)
|
||||
|
@@ -0,0 +1,13 @@
|
||||
package dev.inmo.micro_utils.common.compose
|
||||
|
||||
import androidx.compose.runtime.*
|
||||
import org.jetbrains.compose.web.dom.DOMScope
|
||||
import org.w3c.dom.Element
|
||||
|
||||
fun <TElement : Element> renderComposableAndLinkToRoot(
|
||||
root: TElement,
|
||||
monotonicFrameClock: MonotonicFrameClock = DefaultMonotonicFrameClock,
|
||||
content: @Composable DOMScope<TElement>.() -> Unit
|
||||
): Composition = org.jetbrains.compose.web.renderComposable(root, monotonicFrameClock, content).apply {
|
||||
linkWithElement(root)
|
||||
}
|
@@ -0,0 +1,43 @@
|
||||
package dev.inmo.micro_utils.common.compose
|
||||
|
||||
import org.jetbrains.compose.web.css.*
|
||||
|
||||
object SkeletonAnimation : StyleSheet() {
|
||||
val skeletonKeyFrames: CSSNamedKeyframes by keyframes {
|
||||
to { backgroundPosition("-20% 0") }
|
||||
}
|
||||
|
||||
fun CSSBuilder.includeSkeletonStyle(
|
||||
duration: CSSSizeValue<out CSSUnitTime> = 2.s,
|
||||
timingFunction: AnimationTimingFunction = AnimationTimingFunction.EaseInOut,
|
||||
iterationCount: Int? = null,
|
||||
direction: AnimationDirection = AnimationDirection.Normal,
|
||||
keyFrames: CSSNamedKeyframes = skeletonKeyFrames,
|
||||
hideChildren: Boolean = true,
|
||||
hideText: Boolean = hideChildren
|
||||
) {
|
||||
backgroundImage("linear-gradient(110deg, rgb(236, 236, 236) 40%, rgb(245, 245, 245) 50%, rgb(236, 236, 236) 65%)")
|
||||
backgroundSize("200% 100%")
|
||||
backgroundPosition("180% 0")
|
||||
animation(keyFrames) {
|
||||
duration(duration)
|
||||
timingFunction(timingFunction)
|
||||
iterationCount(iterationCount)
|
||||
direction(direction)
|
||||
}
|
||||
if (hideText) {
|
||||
property("color", "${Color.transparent} !important")
|
||||
}
|
||||
|
||||
if (hideChildren) {
|
||||
child(self, universal) style {
|
||||
property("visibility", "hidden")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val skeleton by style {
|
||||
includeSkeletonStyle()
|
||||
}
|
||||
}
|
||||
|
1
common/compose/src/main/AndroidManifest.xml
Normal file
1
common/compose/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
||||
<manifest package="dev.inmo.micro_utils.common.compose"/>
|
@@ -1,3 +1,5 @@
|
||||
@file:Suppress("OPT_IN_IS_NOT_ENABLED")
|
||||
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
@RequiresOptIn(
|
||||
@@ -12,11 +14,9 @@ package dev.inmo.micro_utils.common
|
||||
AnnotationTarget.PROPERTY_GETTER,
|
||||
AnnotationTarget.PROPERTY_SETTER,
|
||||
AnnotationTarget.FUNCTION,
|
||||
AnnotationTarget.TYPE,
|
||||
AnnotationTarget.TYPEALIAS,
|
||||
AnnotationTarget.TYPE_PARAMETER
|
||||
AnnotationTarget.TYPEALIAS
|
||||
)
|
||||
annotation class PreviewFeature
|
||||
annotation class PreviewFeature(val message: String = "It is possible, that behaviour of this thing will be changed or removed in future releases")
|
||||
|
||||
@RequiresOptIn(
|
||||
"This thing is marked as warned. See message of warn to get more info",
|
||||
@@ -30,8 +30,6 @@ annotation class PreviewFeature
|
||||
AnnotationTarget.PROPERTY_GETTER,
|
||||
AnnotationTarget.PROPERTY_SETTER,
|
||||
AnnotationTarget.FUNCTION,
|
||||
AnnotationTarget.TYPE,
|
||||
AnnotationTarget.TYPEALIAS,
|
||||
AnnotationTarget.TYPE_PARAMETER
|
||||
AnnotationTarget.TYPEALIAS
|
||||
)
|
||||
annotation class Warning(val message: String)
|
||||
|
@@ -1,10 +0,0 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
@Suppress("NOTHING_TO_INLINE")
|
||||
inline fun <T : Comparable<T>> T.clamp(min: T, max: T): T {
|
||||
return when {
|
||||
this < min -> min
|
||||
this > max -> max
|
||||
else -> this
|
||||
}
|
||||
}
|
@@ -2,6 +2,8 @@
|
||||
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
private inline fun <T> getObject(
|
||||
additional: MutableList<T>,
|
||||
iterator: Iterator<T>
|
||||
@@ -14,35 +16,49 @@ private inline fun <T> getObject(
|
||||
/**
|
||||
* Diff object which contains information about differences between two [Iterable]s
|
||||
*
|
||||
* See tests for more info
|
||||
*
|
||||
* @param removed The objects which has been presented in the old collection but absent in new one. Index here is the index in the old collection
|
||||
* @param added The object which appear in new collection only. Indexes here show the index in the new collection
|
||||
* @param replaced Pair of old-new changes. First object has been presented in the old collection on its
|
||||
* [IndexedValue.index] place, the second one is the object in new collection. Both have indexes due to the fact that in
|
||||
* case when some value has been replaced after adds or removes in original collection the object index will be changed
|
||||
*
|
||||
* @see calculateDiff
|
||||
*/
|
||||
@Serializable
|
||||
data class Diff<T> internal constructor(
|
||||
val removed: List<IndexedValue<T>>,
|
||||
val removed: List<@Serializable(IndexedValueSerializer::class) IndexedValue<T>>,
|
||||
/**
|
||||
* Old-New values pairs
|
||||
*/
|
||||
val replaced: List<Pair<IndexedValue<T>, IndexedValue<T>>>,
|
||||
val added: List<IndexedValue<T>>
|
||||
)
|
||||
val replaced: List<Pair<@Serializable(IndexedValueSerializer::class) IndexedValue<T>, @Serializable(IndexedValueSerializer::class) IndexedValue<T>>>,
|
||||
val added: List<@Serializable(IndexedValueSerializer::class) IndexedValue<T>>
|
||||
) {
|
||||
fun isEmpty(): Boolean = removed.isEmpty() && replaced.isEmpty() && added.isEmpty()
|
||||
}
|
||||
|
||||
fun <T> emptyDiff(): Diff<T> = Diff(emptyList(), emptyList(), emptyList())
|
||||
|
||||
private inline fun <T> performChanges(
|
||||
potentialChanges: MutableList<Pair<IndexedValue<T>?, IndexedValue<T>?>>,
|
||||
additionalsInOld: MutableList<T>,
|
||||
additionalsInNew: MutableList<T>,
|
||||
additionsInOld: MutableList<T>,
|
||||
additionsInNew: MutableList<T>,
|
||||
changedList: MutableList<Pair<IndexedValue<T>, IndexedValue<T>>>,
|
||||
removedList: MutableList<IndexedValue<T>>,
|
||||
addedList: MutableList<IndexedValue<T>>,
|
||||
strictComparison: Boolean
|
||||
comparisonFun: (T?, T?) -> Boolean
|
||||
) {
|
||||
var i = -1
|
||||
val (oldObject, newObject) = potentialChanges.lastOrNull() ?: return
|
||||
for ((old, new) in potentialChanges.take(potentialChanges.size - 1)) {
|
||||
i++
|
||||
val oldOneEqualToNewObject = old ?.value === newObject ?.value || (old ?.value == newObject ?.value && !strictComparison)
|
||||
val newOneEqualToOldObject = new ?.value === oldObject ?.value || (new ?.value == oldObject ?.value && !strictComparison)
|
||||
val oldOneEqualToNewObject = comparisonFun(old ?.value, newObject ?.value)
|
||||
val newOneEqualToOldObject = comparisonFun(new ?.value, oldObject ?.value)
|
||||
if (oldOneEqualToNewObject || newOneEqualToOldObject) {
|
||||
changedList.addAll(
|
||||
potentialChanges.take(i).mapNotNull {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
if (it.first != null && it.second != null) it as Pair<IndexedValue<T>, IndexedValue<T>> else null
|
||||
}
|
||||
)
|
||||
@@ -52,20 +68,20 @@ private inline fun <T> performChanges(
|
||||
newPotentials.first().second ?.let { addedList.add(it) }
|
||||
newPotentials.drop(1).take(newPotentials.size - 2).forEach { (oldOne, newOne) ->
|
||||
addedList.add(newOne!!)
|
||||
oldOne ?.let { additionalsInOld.add(oldOne.value) }
|
||||
oldOne ?.let { additionsInOld.add(oldOne.value) }
|
||||
}
|
||||
if (newPotentials.size > 1) {
|
||||
newPotentials.last().first ?.value ?.let { additionalsInOld.add(it) }
|
||||
newPotentials.last().first ?.value ?.let { additionsInOld.add(it) }
|
||||
}
|
||||
}
|
||||
newOneEqualToOldObject -> {
|
||||
newPotentials.first().first ?.let { removedList.add(it) }
|
||||
newPotentials.drop(1).take(newPotentials.size - 2).forEach { (oldOne, newOne) ->
|
||||
removedList.add(oldOne!!)
|
||||
newOne ?.let { additionalsInNew.add(newOne.value) }
|
||||
newOne ?.let { additionsInNew.add(newOne.value) }
|
||||
}
|
||||
if (newPotentials.size > 1) {
|
||||
newPotentials.last().second ?.value ?.let { additionalsInNew.add(it) }
|
||||
newPotentials.last().second ?.value ?.let { additionsInNew.add(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -92,7 +108,7 @@ private inline fun <T> performChanges(
|
||||
*/
|
||||
fun <T> Iterable<T>.calculateDiff(
|
||||
other: Iterable<T>,
|
||||
strictComparison: Boolean = false
|
||||
comparisonFun: (T?, T?) -> Boolean
|
||||
): Diff<T> {
|
||||
var i = -1
|
||||
var j = -1
|
||||
@@ -120,27 +136,60 @@ fun <T> Iterable<T>.calculateDiff(
|
||||
}
|
||||
|
||||
when {
|
||||
oldObject === newObject || (oldObject == newObject && !strictComparison) -> {
|
||||
changedObjects.addAll(potentiallyChangedObjects.map { it as Pair<IndexedValue<T>, IndexedValue<T>> })
|
||||
comparisonFun(oldObject, newObject) -> {
|
||||
changedObjects.addAll(potentiallyChangedObjects.map {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
it as Pair<IndexedValue<T>, IndexedValue<T>>
|
||||
})
|
||||
potentiallyChangedObjects.clear()
|
||||
}
|
||||
else -> {
|
||||
potentiallyChangedObjects.add(oldObject ?.let { IndexedValue(i, oldObject) } to newObject ?.let { IndexedValue(j, newObject) })
|
||||
val previousOldsAdditionsSize = additionalInOld.size
|
||||
val previousNewsAdditionsSize = additionalInNew.size
|
||||
performChanges(potentiallyChangedObjects, additionalInOld, additionalInNew, changedObjects, removedObjects, addedObjects, strictComparison)
|
||||
performChanges(potentiallyChangedObjects, additionalInOld, additionalInNew, changedObjects, removedObjects, addedObjects, comparisonFun)
|
||||
i -= (additionalInOld.size - previousOldsAdditionsSize)
|
||||
j -= (additionalInNew.size - previousNewsAdditionsSize)
|
||||
}
|
||||
}
|
||||
}
|
||||
potentiallyChangedObjects.add(null to null)
|
||||
performChanges(potentiallyChangedObjects, additionalInOld, additionalInNew, changedObjects, removedObjects, addedObjects, strictComparison)
|
||||
performChanges(potentiallyChangedObjects, additionalInOld, additionalInNew, changedObjects, removedObjects, addedObjects, comparisonFun)
|
||||
|
||||
return Diff(removedObjects.toList(), changedObjects.toList(), addedObjects.toList())
|
||||
}
|
||||
|
||||
inline fun <T> Diff(old: Iterable<T>, new: Iterable<T>) = old.calculateDiff(new)
|
||||
/**
|
||||
* Calculating [Diff] object
|
||||
*
|
||||
* @param strictComparison If this parameter set to true, objects which are not equal by links will be used as different
|
||||
* objects. For example, in case of two "Example" string they will be equal by value, but CAN be different by links
|
||||
*/
|
||||
fun <T> Iterable<T>.calculateDiff(
|
||||
other: Iterable<T>,
|
||||
strictComparison: Boolean = false
|
||||
): Diff<T> = calculateDiff(
|
||||
other,
|
||||
comparisonFun = if (strictComparison) {
|
||||
{ t1, t2 ->
|
||||
t1 === t2
|
||||
}
|
||||
} else {
|
||||
{ t1, t2 ->
|
||||
t1 === t2 || t1 == t2 // small optimization for cases when t1 and t2 are the same - comparison will be faster potentially
|
||||
}
|
||||
}
|
||||
)
|
||||
inline fun <T> Iterable<T>.diff(
|
||||
other: Iterable<T>,
|
||||
strictComparison: Boolean = false
|
||||
): Diff<T> = calculateDiff(other, strictComparison)
|
||||
inline fun <T> Iterable<T>.diff(
|
||||
other: Iterable<T>,
|
||||
noinline comparisonFun: (T?, T?) -> Boolean
|
||||
): Diff<T> = calculateDiff(other, comparisonFun)
|
||||
|
||||
inline fun <T> Diff(old: Iterable<T>, new: Iterable<T>) = old.calculateDiff(new, strictComparison = false)
|
||||
inline fun <T> StrictDiff(old: Iterable<T>, new: Iterable<T>) = old.calculateDiff(new, true)
|
||||
|
||||
/**
|
||||
@@ -149,3 +198,52 @@ inline fun <T> StrictDiff(old: Iterable<T>, new: Iterable<T>) = old.calculateDif
|
||||
inline fun <T> Iterable<T>.calculateStrictDiff(
|
||||
other: Iterable<T>
|
||||
) = calculateDiff(other, strictComparison = true)
|
||||
|
||||
/**
|
||||
* Applies [diff] to [this] [MutableList]
|
||||
*/
|
||||
fun <T> MutableList<T>.applyDiff(
|
||||
diff: Diff<T>
|
||||
) {
|
||||
for (i in diff.removed.indices.sortedDescending()) {
|
||||
removeAt(diff.removed[i].index)
|
||||
}
|
||||
diff.added.forEach { (i, t) ->
|
||||
add(i, t)
|
||||
}
|
||||
diff.replaced.forEach { (_, new) ->
|
||||
set(new.index, new.value)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method call [calculateDiff] with strict mode [strictComparison] and then apply differences to [this]
|
||||
* mutable list
|
||||
*/
|
||||
fun <T> MutableList<T>.applyDiff(
|
||||
source: Iterable<T>,
|
||||
strictComparison: Boolean = false
|
||||
): Diff<T> = calculateDiff(source, strictComparison).also {
|
||||
applyDiff(it)
|
||||
}
|
||||
|
||||
/**
|
||||
* This method call [calculateDiff] and then apply differences to [this]
|
||||
* mutable list
|
||||
*/
|
||||
fun <T> MutableList<T>.applyDiff(
|
||||
source: Iterable<T>,
|
||||
comparisonFun: (T?, T?) -> Boolean
|
||||
): Diff<T> = calculateDiff(source, comparisonFun).also {
|
||||
applyDiff(it)
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse [this] [Diff]. Result will contain [Diff.added] on [Diff.removed] (and vice-verse), all the
|
||||
* [Diff.replaced] values will be reversed too
|
||||
*/
|
||||
fun <T> Diff<T>.reversed() = Diff(
|
||||
removed = added,
|
||||
replaced = replaced.map { it.second to it.first },
|
||||
added = removed
|
||||
)
|
||||
|
@@ -0,0 +1,160 @@
|
||||
@file:Suppress("unused", "NOTHING_TO_INLINE")
|
||||
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
import kotlinx.serialization.*
|
||||
import kotlinx.serialization.builtins.serializer
|
||||
import kotlinx.serialization.descriptors.*
|
||||
import kotlinx.serialization.encoding.*
|
||||
|
||||
/**
|
||||
* Realization of this interface will contains at least one not null - [optionalT1] or [optionalT2]
|
||||
*
|
||||
* @see EitherFirst
|
||||
* @see EitherSecond
|
||||
* @see Either.Companion.first
|
||||
* @see Either.Companion.second
|
||||
* @see Either.onFirst
|
||||
* @see Either.onSecond
|
||||
* @see Either.mapOnFirst
|
||||
* @see Either.mapOnSecond
|
||||
*/
|
||||
@Serializable(EitherSerializer::class)
|
||||
sealed interface Either<T1, T2> {
|
||||
val optionalT1: Optional<T1>
|
||||
val optionalT2: Optional<T2>
|
||||
|
||||
val t1OrNull: T1?
|
||||
get() = optionalT1.dataOrNull()
|
||||
val t2OrNull: T2?
|
||||
get() = optionalT2.dataOrNull()
|
||||
}
|
||||
|
||||
class EitherSerializer<T1, T2>(
|
||||
t1Serializer: KSerializer<T1>,
|
||||
t2Serializer: KSerializer<T2>,
|
||||
) : KSerializer<Either<T1, T2>> {
|
||||
@OptIn(InternalSerializationApi::class, ExperimentalSerializationApi::class)
|
||||
override val descriptor: SerialDescriptor = buildSerialDescriptor(
|
||||
"TypedSerializer",
|
||||
SerialKind.CONTEXTUAL
|
||||
) {
|
||||
element("type", String.serializer().descriptor)
|
||||
element("value", ContextualSerializer(Either::class).descriptor)
|
||||
}
|
||||
private val t1EitherSerializer = EitherFirst.serializer(t1Serializer, t2Serializer)
|
||||
private val t2EitherSerializer = EitherSecond.serializer(t1Serializer, t2Serializer)
|
||||
|
||||
override fun deserialize(decoder: Decoder): Either<T1, T2> {
|
||||
return decoder.decodeStructure(descriptor) {
|
||||
var type: String? = null
|
||||
lateinit var result: Either<T1, T2>
|
||||
while (true) {
|
||||
when (val index = decodeElementIndex(descriptor)) {
|
||||
0 -> type = decodeStringElement(descriptor, 0)
|
||||
1 -> {
|
||||
result = when (type) {
|
||||
"t1" -> decodeSerializableElement(
|
||||
descriptor,
|
||||
1,
|
||||
t1EitherSerializer
|
||||
)
|
||||
"t2" -> decodeSerializableElement(
|
||||
descriptor,
|
||||
1,
|
||||
t2EitherSerializer
|
||||
)
|
||||
else -> error("Unknown type of either: $type")
|
||||
}
|
||||
}
|
||||
CompositeDecoder.DECODE_DONE -> break
|
||||
else -> error("Unexpected index: $index")
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun serialize(encoder: Encoder, value: Either<T1, T2>) {
|
||||
encoder.encodeStructure(descriptor) {
|
||||
when (value) {
|
||||
is EitherFirst -> {
|
||||
encodeStringElement(descriptor, 0, "t1")
|
||||
encodeSerializableElement(descriptor, 1, t1EitherSerializer, value)
|
||||
}
|
||||
is EitherSecond -> {
|
||||
encodeStringElement(descriptor, 0, "t2")
|
||||
encodeSerializableElement(descriptor, 1, t2EitherSerializer, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This type [Either] will always have not nullable [optionalT1]
|
||||
*/
|
||||
@Serializable
|
||||
data class EitherFirst<T1, T2>(
|
||||
val t1: T1
|
||||
) : Either<T1, T2> {
|
||||
override val optionalT1: Optional<T1> = t1.optional
|
||||
override val optionalT2: Optional<T2> = Optional.absent()
|
||||
}
|
||||
|
||||
/**
|
||||
* This type [Either] will always have not nullable [optionalT2]
|
||||
*/
|
||||
@Serializable
|
||||
data class EitherSecond<T1, T2>(
|
||||
val t2: T2
|
||||
) : Either<T1, T2> {
|
||||
override val optionalT1: Optional<T1> = Optional.absent()
|
||||
override val optionalT2: Optional<T2> = t2.optional
|
||||
}
|
||||
|
||||
/**
|
||||
* @return New instance of [EitherFirst]
|
||||
*/
|
||||
inline fun <T1, T2> Either.Companion.first(t1: T1): Either<T1, T2> = EitherFirst(t1)
|
||||
/**
|
||||
* @return New instance of [EitherSecond]
|
||||
*/
|
||||
inline fun <T1, T2> Either.Companion.second(t2: T2): Either<T1, T2> = EitherSecond(t2)
|
||||
|
||||
/**
|
||||
* Will call [block] in case when [this] is [EitherFirst]
|
||||
*/
|
||||
inline fun <T1, T2, E : Either<T1, T2>> E.onFirst(block: (T1) -> Unit): E {
|
||||
optionalT1.onPresented(block)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Will call [block] in case when [this] is [EitherSecond]
|
||||
*/
|
||||
inline fun <T1, T2, E : Either<T1, T2>> E.onSecond(block: (T2) -> Unit): E {
|
||||
optionalT2.onPresented(block)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Result of [block] if [this] is [EitherFirst]
|
||||
*/
|
||||
inline fun <T1, R> Either<T1, *>.mapOnFirst(block: (T1) -> R): R? {
|
||||
return optionalT1.mapOnPresented(block)
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Result of [block] if [this] is [EitherSecond]
|
||||
*/
|
||||
inline fun <T2, R> Either<*, T2>.mapOnSecond(block: (T2) -> R): R? {
|
||||
return optionalT2.mapOnPresented(block)
|
||||
}
|
||||
|
||||
inline fun <reified T1, reified T2> Any.either() = when (this) {
|
||||
is T1 -> Either.first<T1, T2>(this)
|
||||
is T2 -> Either.second<T1, T2>(this)
|
||||
else -> error("Incorrect type of either argument $this")
|
||||
}
|
@@ -0,0 +1,43 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
inline fun <T> Boolean.letIfTrue(block: () -> T): T? {
|
||||
return if (this) {
|
||||
block()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <T> Boolean.letIfFalse(block: () -> T): T? {
|
||||
return if (this) {
|
||||
null
|
||||
} else {
|
||||
block()
|
||||
}
|
||||
}
|
||||
|
||||
inline fun Boolean.alsoIfTrue(block: () -> Unit): Boolean {
|
||||
letIfTrue(block)
|
||||
return this
|
||||
}
|
||||
|
||||
inline fun Boolean.alsoIfFalse(block: () -> Unit): Boolean {
|
||||
letIfFalse(block)
|
||||
return this
|
||||
}
|
||||
|
||||
inline fun <T> Boolean.ifTrue(block: () -> T): T? {
|
||||
return if (this) {
|
||||
block()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <T> Boolean.ifFalse(block: () -> T): T? {
|
||||
return if (this) {
|
||||
null
|
||||
} else {
|
||||
block()
|
||||
}
|
||||
}
|
@@ -0,0 +1,30 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.Serializer
|
||||
import kotlinx.serialization.builtins.PairSerializer
|
||||
import kotlinx.serialization.builtins.serializer
|
||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||
import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
|
||||
class IndexedValueSerializer<T>(private val subSerializer: KSerializer<T>) : KSerializer<IndexedValue<T>> {
|
||||
private val originalSerializer = PairSerializer(Int.serializer(), subSerializer)
|
||||
override val descriptor: SerialDescriptor
|
||||
get() = originalSerializer.descriptor
|
||||
|
||||
override fun deserialize(decoder: Decoder): IndexedValue<T> {
|
||||
val pair = originalSerializer.deserialize(decoder)
|
||||
return IndexedValue(
|
||||
pair.first,
|
||||
pair.second
|
||||
)
|
||||
}
|
||||
|
||||
override fun serialize(encoder: Encoder, value: IndexedValue<T>) {
|
||||
originalSerializer.serialize(
|
||||
encoder,
|
||||
Pair(value.index, value.value)
|
||||
)
|
||||
}
|
||||
}
|
@@ -7,9 +7,17 @@ import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
|
||||
typealias ByteArrayAllocator = () -> ByteArray
|
||||
typealias SuspendByteArrayAllocator = suspend () -> ByteArray
|
||||
|
||||
val ByteArray.asAllocator: ByteArrayAllocator
|
||||
get() = { this }
|
||||
val ByteArray.asSuspendAllocator: SuspendByteArrayAllocator
|
||||
get() = { this }
|
||||
val ByteArrayAllocator.asSuspendAllocator: SuspendByteArrayAllocator
|
||||
get() = { this() }
|
||||
suspend fun SuspendByteArrayAllocator.asAllocator(): ByteArrayAllocator {
|
||||
return invoke().asAllocator
|
||||
}
|
||||
|
||||
object ByteArrayAllocatorSerializer : KSerializer<ByteArrayAllocator> {
|
||||
private val realSerializer = ByteArraySerializer()
|
||||
@@ -17,7 +25,7 @@ object ByteArrayAllocatorSerializer : KSerializer<ByteArrayAllocator> {
|
||||
|
||||
override fun deserialize(decoder: Decoder): ByteArrayAllocator {
|
||||
val bytes = realSerializer.deserialize(decoder)
|
||||
return { bytes }
|
||||
return bytes.asAllocator
|
||||
}
|
||||
|
||||
override fun serialize(encoder: Encoder, value: ByteArrayAllocator) {
|
||||
|
@@ -0,0 +1,59 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
inline fun <I, R> Iterable<I>.joinTo(
|
||||
separatorFun: (I) -> R?,
|
||||
prefix: R? = null,
|
||||
postfix: R? = null,
|
||||
transform: (I) -> R?
|
||||
): List<R> {
|
||||
val result = mutableListOf<R>()
|
||||
val iterator = iterator()
|
||||
|
||||
prefix ?.let(result::add)
|
||||
|
||||
while (iterator.hasNext()) {
|
||||
val element = iterator.next()
|
||||
result.add(transform(element) ?: continue)
|
||||
|
||||
if (iterator.hasNext()) {
|
||||
result.add(separatorFun(element) ?: continue)
|
||||
}
|
||||
}
|
||||
|
||||
postfix ?.let(result::add)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
inline fun <I, R> Iterable<I>.joinTo(
|
||||
separator: R? = null,
|
||||
prefix: R? = null,
|
||||
postfix: R? = null,
|
||||
transform: (I) -> R?
|
||||
): List<R> = joinTo({ separator }, prefix, postfix, transform)
|
||||
|
||||
inline fun <I> Iterable<I>.joinTo(
|
||||
separatorFun: (I) -> I?,
|
||||
prefix: I? = null,
|
||||
postfix: I? = null
|
||||
): List<I> = joinTo<I, I>(separatorFun, prefix, postfix) { it }
|
||||
|
||||
inline fun <I> Iterable<I>.joinTo(
|
||||
separator: I? = null,
|
||||
prefix: I? = null,
|
||||
postfix: I? = null
|
||||
): List<I> = joinTo<I>({ separator }, prefix, postfix)
|
||||
|
||||
inline fun <I, reified R> Array<I>.joinTo(
|
||||
separatorFun: (I) -> R?,
|
||||
prefix: R? = null,
|
||||
postfix: R? = null,
|
||||
transform: (I) -> R?
|
||||
): Array<R> = asIterable().joinTo(separatorFun, prefix, postfix, transform).toTypedArray()
|
||||
|
||||
inline fun <I, reified R> Array<I>.joinTo(
|
||||
separator: R? = null,
|
||||
prefix: R? = null,
|
||||
postfix: R? = null,
|
||||
transform: (I) -> R?
|
||||
): Array<R> = asIterable().joinTo(separator, prefix, postfix, transform).toTypedArray()
|
@@ -0,0 +1,34 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlin.jvm.JvmInline
|
||||
|
||||
@Serializable
|
||||
@JvmInline
|
||||
value class FileName(val string: String) {
|
||||
val name: String
|
||||
get() = withoutSlashAtTheEnd.takeLastWhile { it != '/' }
|
||||
val extension: String
|
||||
get() = name.takeLastWhile { it != '.' }
|
||||
val nameWithoutExtension: String
|
||||
get() {
|
||||
val filename = name
|
||||
return filename.indexOfLast { it == '.' }.takeIf { it > -1 } ?.let {
|
||||
filename.substring(0, it)
|
||||
} ?: filename
|
||||
}
|
||||
val withoutSlashAtTheEnd: String
|
||||
get() = string.dropLastWhile { it == '/' }
|
||||
override fun toString(): String = string
|
||||
}
|
||||
|
||||
|
||||
expect class MPPFile
|
||||
|
||||
expect val MPPFile.filename: FileName
|
||||
expect val MPPFile.filesize: Long
|
||||
expect val MPPFile.bytesAllocatorSync: ByteArrayAllocator
|
||||
expect val MPPFile.bytesAllocator: SuspendByteArrayAllocator
|
||||
fun MPPFile.bytesSync() = bytesAllocatorSync()
|
||||
suspend fun MPPFile.bytes() = bytesAllocator()
|
||||
|
@@ -0,0 +1,135 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
/**
|
||||
* Contains diff based on the comparison of objects with the same [K].
|
||||
*
|
||||
* @param removed Contains map with keys removed from parent map
|
||||
* @param changed Contains map with keys values changed new map in comparison with old one
|
||||
* @param added Contains map with new keys and values
|
||||
*/
|
||||
data class MapDiff<K, V> @Warning(warning) constructor(
|
||||
val removed: Map<K, V>,
|
||||
val changed: Map<K, Pair<V, V>>,
|
||||
val added: Map<K, V>
|
||||
) {
|
||||
fun isEmpty() = removed.isEmpty() && changed.isEmpty() && added.isEmpty()
|
||||
inline fun isNotEmpty() = !isEmpty()
|
||||
|
||||
companion object {
|
||||
private const val warning = "This feature can be changed without any warranties. Use with caution and only in case you know what you are doing"
|
||||
fun <K, V> empty() = MapDiff<K, V>(emptyMap(), emptyMap(), emptyMap())
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun <K, V> createCompareFun(
|
||||
strictComparison: Boolean
|
||||
): (K, V, V) -> Boolean = if (strictComparison) {
|
||||
{ _, first, second -> first === second }
|
||||
} else {
|
||||
{ _, first, second -> first == second }
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare [this] [Map] with the [other] one in principle when [other] is newer than [this]
|
||||
*
|
||||
* @param compareFun Will be used to determine changed values
|
||||
*/
|
||||
fun <K, V> Map<K, V>.diff(
|
||||
other: Map<K, V>,
|
||||
compareFun: (K, V, V) -> Boolean
|
||||
): MapDiff<K, V> {
|
||||
val removed: Map<K, V> = (keys - other.keys).associateWith {
|
||||
getValue(it)
|
||||
}
|
||||
val added: Map<K, V> = (other.keys - keys).associateWith {
|
||||
other.getValue(it)
|
||||
}
|
||||
val changed = keys.intersect(other.keys).mapNotNull {
|
||||
val old = getValue(it)
|
||||
val new = other.getValue(it)
|
||||
if (compareFun(it, old, new)) {
|
||||
return@mapNotNull null
|
||||
} else {
|
||||
it to (old to new)
|
||||
}
|
||||
}.toMap()
|
||||
|
||||
return MapDiff(
|
||||
removed,
|
||||
changed,
|
||||
added
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare [this] [Map] with the [other] one in principle when [other] is newer than [this]
|
||||
*
|
||||
* @param strictComparison If true, will use strict (===) comparison for the values' comparison. Otherwise, standard
|
||||
* `equals` will be used
|
||||
*/
|
||||
fun <K, V> Map<K, V>.diff(
|
||||
other: Map<K, V>,
|
||||
strictComparison: Boolean = false
|
||||
): MapDiff<K, V> = diff(
|
||||
other,
|
||||
compareFun = createCompareFun(strictComparison)
|
||||
)
|
||||
|
||||
/**
|
||||
* Will apply [mapDiff] to [this] [MutableMap]
|
||||
*/
|
||||
fun <K, V> MutableMap<K, V>.applyDiff(
|
||||
mapDiff: MapDiff<K, V>
|
||||
) {
|
||||
mapDiff.apply {
|
||||
removed.keys.forEach { remove(it) }
|
||||
changed.forEach { (k, oldNew) ->
|
||||
put(k, oldNew.second)
|
||||
}
|
||||
added.forEach { (k, new) ->
|
||||
put(k, new)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Will apply changes with [from] map into [this] one
|
||||
*
|
||||
* @param compareFun Will be used to determine changed values
|
||||
*
|
||||
* @return [MapDiff] applied to [this] [MutableMap]
|
||||
*/
|
||||
fun <K, V> MutableMap<K, V>.applyDiff(
|
||||
from: Map<K, V>,
|
||||
compareFun: (K, V, V) -> Boolean
|
||||
): MapDiff<K, V> {
|
||||
return diff(from, compareFun).also {
|
||||
applyDiff(it)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Will apply changes with [from] map into [this] one
|
||||
*
|
||||
* @param strictComparison If true, will use strict (===) comparison for the values' comparison. Otherwise, standard
|
||||
* `equals` will be used
|
||||
*
|
||||
* @return [MapDiff] applied to [this] [MutableMap]
|
||||
*/
|
||||
fun <K, V> MutableMap<K, V>.applyDiff(
|
||||
from: Map<K, V>,
|
||||
strictComparison: Boolean = false
|
||||
): MapDiff<K, V> = applyDiff(
|
||||
from,
|
||||
compareFun = createCompareFun(strictComparison)
|
||||
)
|
||||
|
||||
/**
|
||||
* Reverse [this] [MapDiff]. Result will contain [MapDiff.added] on [MapDiff.removed] (and vice-verse), all the
|
||||
* [MapDiff.changed] values will be reversed too
|
||||
*/
|
||||
fun <K, V> MapDiff<K, V>.reversed(): MapDiff<K, V> = MapDiff(
|
||||
removed = added,
|
||||
changed = changed.mapValues { (_, oldNew) -> oldNew.second to oldNew.first },
|
||||
added = removed
|
||||
)
|
@@ -0,0 +1,53 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
import kotlin.jvm.JvmName
|
||||
|
||||
interface SimpleMapper<T1, T2> {
|
||||
fun convertToT1(from: T2): T1
|
||||
fun convertToT2(from: T1): T2
|
||||
}
|
||||
|
||||
@JvmName("convertFromT2")
|
||||
fun <T1, T2> SimpleMapper<T1, T2>.convert(from: T2) = convertToT1(from)
|
||||
@JvmName("convertFromT1")
|
||||
fun <T1, T2> SimpleMapper<T1, T2>.convert(from: T1) = convertToT2(from)
|
||||
|
||||
class SimpleMapperImpl<T1, T2>(
|
||||
private val t1: (T2) -> T1,
|
||||
private val t2: (T1) -> T2,
|
||||
) : SimpleMapper<T1, T2> {
|
||||
override fun convertToT1(from: T2): T1 = t1.invoke(from)
|
||||
|
||||
override fun convertToT2(from: T1): T2 = t2.invoke(from)
|
||||
}
|
||||
|
||||
@Suppress("NOTHING_TO_INLINE")
|
||||
inline fun <T1, T2> simpleMapper(
|
||||
noinline t1: (T2) -> T1,
|
||||
noinline t2: (T1) -> T2,
|
||||
) = SimpleMapperImpl(t1, t2)
|
||||
|
||||
interface SimpleSuspendableMapper<T1, T2> {
|
||||
suspend fun convertToT1(from: T2): T1
|
||||
suspend fun convertToT2(from: T1): T2
|
||||
}
|
||||
|
||||
@JvmName("convertFromT2")
|
||||
suspend fun <T1, T2> SimpleSuspendableMapper<T1, T2>.convert(from: T2) = convertToT1(from)
|
||||
@JvmName("convertFromT1")
|
||||
suspend fun <T1, T2> SimpleSuspendableMapper<T1, T2>.convert(from: T1) = convertToT2(from)
|
||||
|
||||
class SimpleSuspendableMapperImpl<T1, T2>(
|
||||
private val t1: suspend (T2) -> T1,
|
||||
private val t2: suspend (T1) -> T2,
|
||||
) : SimpleSuspendableMapper<T1, T2> {
|
||||
override suspend fun convertToT1(from: T2): T1 = t1.invoke(from)
|
||||
|
||||
override suspend fun convertToT2(from: T1): T2 = t2.invoke(from)
|
||||
}
|
||||
|
||||
@Suppress("NOTHING_TO_INLINE")
|
||||
inline fun <T1, T2> simpleSuspendableMapper(
|
||||
noinline t1: suspend (T2) -> T1,
|
||||
noinline t2: suspend (T1) -> T2,
|
||||
) = SimpleSuspendableMapperImpl(t1, t2)
|
@@ -0,0 +1,97 @@
|
||||
@file:Suppress("unused")
|
||||
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* This type represents [T] as not only potentially nullable data, but also as a data which can not be presented. This
|
||||
* type will be useful in cases when [T] is nullable and null as valuable data too in time of data absence should be
|
||||
* presented by some third type.
|
||||
*
|
||||
* Let's imagine, you have nullable name in some database. In case when name is not nullable everything is clear - null
|
||||
* will represent absence of row in the database. In case when name is nullable null will be a little bit dual-meaning,
|
||||
* cause this null will say nothing about availability of the row (of course, it is exaggerated example)
|
||||
*
|
||||
* @see Optional.presented
|
||||
* @see Optional.absent
|
||||
* @see Optional.optional
|
||||
* @see Optional.onPresented
|
||||
* @see Optional.onAbsent
|
||||
*/
|
||||
@Serializable
|
||||
data class Optional<T> internal constructor(
|
||||
@Warning("It is unsafe to use this data directly")
|
||||
val data: T?,
|
||||
@Warning("It is unsafe to use this data directly")
|
||||
val dataPresented: Boolean
|
||||
) {
|
||||
companion object {
|
||||
/**
|
||||
* Will create [Optional] with presented data
|
||||
*/
|
||||
fun <T> presented(data: T) = Optional(data, true)
|
||||
/**
|
||||
* Will create [Optional] without data
|
||||
*/
|
||||
fun <T> absent() = Optional<T>(null, false)
|
||||
}
|
||||
}
|
||||
|
||||
inline val <T> T.optional
|
||||
get() = Optional.presented(this)
|
||||
|
||||
inline val <T : Any> T?.optionalOrAbsentIfNull
|
||||
get() = if (this == null) {
|
||||
Optional.absent<T>()
|
||||
} else {
|
||||
Optional.presented(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Will call [block] when data presented ([Optional.dataPresented] == true)
|
||||
*/
|
||||
inline fun <T> Optional<T>.onPresented(block: (T) -> Unit): Optional<T> = apply {
|
||||
@OptIn(Warning::class)
|
||||
if (dataPresented) { @Suppress("UNCHECKED_CAST") block(data as T) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Will call [block] when data presented ([Optional.dataPresented] == true)
|
||||
*/
|
||||
inline fun <T, R> Optional<T>.mapOnPresented(block: (T) -> R): R? = run {
|
||||
@OptIn(Warning::class)
|
||||
if (dataPresented) { @Suppress("UNCHECKED_CAST") block(data as T) } else null
|
||||
}
|
||||
|
||||
/**
|
||||
* Will call [block] when data absent ([Optional.dataPresented] == false)
|
||||
*/
|
||||
inline fun <T> Optional<T>.onAbsent(block: () -> Unit): Optional<T> = apply {
|
||||
@OptIn(Warning::class)
|
||||
if (!dataPresented) { block() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Will call [block] when data presented ([Optional.dataPresented] == true)
|
||||
*/
|
||||
inline fun <T, R> Optional<T>.mapOnAbsent(block: () -> R): R? = run {
|
||||
@OptIn(Warning::class)
|
||||
if (!dataPresented) { block() } else null
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns [Optional.data] if [Optional.dataPresented] of [this] is true, or null otherwise
|
||||
*/
|
||||
fun <T> Optional<T>.dataOrNull() = @OptIn(Warning::class) if (dataPresented) @Suppress("UNCHECKED_CAST") (data as T) else null
|
||||
|
||||
/**
|
||||
* Returns [Optional.data] if [Optional.dataPresented] of [this] is true, or throw [throwable] otherwise
|
||||
*/
|
||||
fun <T> Optional<T>.dataOrThrow(throwable: Throwable) = @OptIn(Warning::class) if (dataPresented) @Suppress("UNCHECKED_CAST") (data as T) else throw throwable
|
||||
|
||||
|
||||
/**
|
||||
* Returns [Optional.data] if [Optional.dataPresented] of [this] is true, or call [block] and returns the result of it
|
||||
*/
|
||||
inline fun <T> Optional<T>.dataOrElse(block: () -> T) = @OptIn(Warning::class) if (dataPresented) @Suppress("UNCHECKED_CAST") (data as T) else block()
|
@@ -0,0 +1,28 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
/**
|
||||
* Convert [this] [Long] to [Int] with bounds of [Int.MIN_VALUE] and [Int.MAX_VALUE]
|
||||
*/
|
||||
fun Long.toCoercedInt(): Int = coerceIn(Int.MIN_VALUE.toLong(), Int.MAX_VALUE.toLong()).toInt()
|
||||
/**
|
||||
* Convert [this] [Long] to [Short] with bounds of [Short.MIN_VALUE] and [Short.MAX_VALUE]
|
||||
*/
|
||||
fun Long.toCoercedShort(): Short = coerceIn(Short.MIN_VALUE.toLong(), Short.MAX_VALUE.toLong()).toShort()
|
||||
/**
|
||||
* Convert [this] [Long] to [Byte] with bounds of [Byte.MIN_VALUE] and [Byte.MAX_VALUE]
|
||||
*/
|
||||
fun Long.toCoercedByte(): Byte = coerceIn(Byte.MIN_VALUE.toLong(), Byte.MAX_VALUE.toLong()).toByte()
|
||||
|
||||
/**
|
||||
* Convert [this] [Int] to [Short] with bounds of [Short.MIN_VALUE] and [Short.MAX_VALUE]
|
||||
*/
|
||||
fun Int.toCoercedShort(): Short = coerceIn(Short.MIN_VALUE.toInt(), Short.MAX_VALUE.toInt()).toShort()
|
||||
/**
|
||||
* Convert [this] [Int] to [Byte] with bounds of [Byte.MIN_VALUE] and [Byte.MAX_VALUE]
|
||||
*/
|
||||
fun Int.toCoercedByte(): Byte = coerceIn(Byte.MIN_VALUE.toInt(), Byte.MAX_VALUE.toInt()).toByte()
|
||||
|
||||
/**
|
||||
* Convert [this] [Short] to [Byte] with bounds of [Byte.MIN_VALUE] and [Byte.MAX_VALUE]
|
||||
*/
|
||||
fun Short.toCoercedByte(): Byte = coerceIn(Byte.MIN_VALUE.toShort(), Byte.MAX_VALUE.toShort()).toByte()
|
@@ -0,0 +1,37 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlin.jvm.JvmInline
|
||||
|
||||
@Serializable
|
||||
@JvmInline
|
||||
value class Progress private constructor(
|
||||
val of1: Double
|
||||
) {
|
||||
val of1Float
|
||||
get() = of1.toFloat()
|
||||
val of100
|
||||
get() = of1 * 100
|
||||
val of100Float
|
||||
get() = of100.toFloat()
|
||||
val of100Int
|
||||
get() = of100.toInt()
|
||||
|
||||
init {
|
||||
require(of1 in rangeOfValues) {
|
||||
"Progress main value should be in $rangeOfValues, but incoming value is $of1"
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val rangeOfValues = 0.0 .. 1.0
|
||||
|
||||
val START = Progress(rangeOfValues.start)
|
||||
val COMPLETED = Progress(rangeOfValues.endInclusive)
|
||||
|
||||
operator fun invoke(of1: Double) = Progress(of1.coerceIn(rangeOfValues))
|
||||
operator fun invoke(part: Number, total: Number) = Progress(
|
||||
part.toDouble() / total.toDouble()
|
||||
)
|
||||
}
|
||||
}
|
@@ -0,0 +1,80 @@
|
||||
@file:Suppress(
|
||||
"RemoveRedundantCallsOfConversionMethods",
|
||||
"RedundantVisibilityModifier",
|
||||
)
|
||||
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
import kotlin.Byte
|
||||
import kotlin.Double
|
||||
import kotlin.Float
|
||||
import kotlin.Int
|
||||
import kotlin.Long
|
||||
import kotlin.Short
|
||||
import kotlin.Suppress
|
||||
|
||||
public operator fun Progress.plus(other: Progress): Progress = Progress(of1 + other.of1)
|
||||
|
||||
public operator fun Progress.minus(other: Progress): Progress = Progress(of1 - other.of1)
|
||||
|
||||
public operator fun Progress.plus(i: Byte): Progress = Progress((of1 + i).toDouble())
|
||||
|
||||
public operator fun Progress.minus(i: Byte): Progress = Progress((of1 - i).toDouble())
|
||||
|
||||
public operator fun Progress.times(i: Byte): Progress = Progress((of1 * i).toDouble())
|
||||
|
||||
public operator fun Progress.div(i: Byte): Progress = Progress((of1 / i).toDouble())
|
||||
|
||||
public operator fun Progress.rem(i: Byte): Progress = Progress((of1 % i).toDouble())
|
||||
|
||||
public operator fun Progress.plus(i: Short): Progress = Progress((of1 + i).toDouble())
|
||||
|
||||
public operator fun Progress.minus(i: Short): Progress = Progress((of1 - i).toDouble())
|
||||
|
||||
public operator fun Progress.times(i: Short): Progress = Progress((of1 * i).toDouble())
|
||||
|
||||
public operator fun Progress.div(i: Short): Progress = Progress((of1 / i).toDouble())
|
||||
|
||||
public operator fun Progress.rem(i: Short): Progress = Progress((of1 % i).toDouble())
|
||||
|
||||
public operator fun Progress.plus(i: Int): Progress = Progress((of1 + i).toDouble())
|
||||
|
||||
public operator fun Progress.minus(i: Int): Progress = Progress((of1 - i).toDouble())
|
||||
|
||||
public operator fun Progress.times(i: Int): Progress = Progress((of1 * i).toDouble())
|
||||
|
||||
public operator fun Progress.div(i: Int): Progress = Progress((of1 / i).toDouble())
|
||||
|
||||
public operator fun Progress.rem(i: Int): Progress = Progress((of1 % i).toDouble())
|
||||
|
||||
public operator fun Progress.plus(i: Long): Progress = Progress((of1 + i).toDouble())
|
||||
|
||||
public operator fun Progress.minus(i: Long): Progress = Progress((of1 - i).toDouble())
|
||||
|
||||
public operator fun Progress.times(i: Long): Progress = Progress((of1 * i).toDouble())
|
||||
|
||||
public operator fun Progress.div(i: Long): Progress = Progress((of1 / i).toDouble())
|
||||
|
||||
public operator fun Progress.rem(i: Long): Progress = Progress((of1 % i).toDouble())
|
||||
|
||||
public operator fun Progress.plus(i: Float): Progress = Progress((of1 + i).toDouble())
|
||||
|
||||
public operator fun Progress.minus(i: Float): Progress = Progress((of1 - i).toDouble())
|
||||
|
||||
public operator fun Progress.times(i: Float): Progress = Progress((of1 * i).toDouble())
|
||||
|
||||
public operator fun Progress.div(i: Float): Progress = Progress((of1 / i).toDouble())
|
||||
|
||||
public operator fun Progress.rem(i: Float): Progress = Progress((of1 % i).toDouble())
|
||||
|
||||
public operator fun Progress.plus(i: Double): Progress = Progress((of1 + i).toDouble())
|
||||
|
||||
public operator fun Progress.minus(i: Double): Progress = Progress((of1 - i).toDouble())
|
||||
|
||||
public operator fun Progress.times(i: Double): Progress = Progress((of1 * i).toDouble())
|
||||
|
||||
public operator fun Progress.div(i: Double): Progress = Progress((of1 / i).toDouble())
|
||||
|
||||
public operator fun Progress.rem(i: Double): Progress = Progress((of1 % i).toDouble())
|
||||
|
||||
public operator fun Progress.compareTo(other: Progress): Int = (of1 - other.of1).toInt()
|
@@ -0,0 +1,19 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
fun <T : Comparable<T>> ClosedRange<T>.intersect(other: ClosedRange<T>): Pair<T, T>? = when {
|
||||
start == other.start && endInclusive == other.endInclusive -> start to endInclusive
|
||||
start > other.endInclusive || other.start > endInclusive -> null
|
||||
else -> maxOf(start, other.start) to minOf(endInclusive, other.endInclusive)
|
||||
}
|
||||
|
||||
fun IntRange.intersect(
|
||||
other: IntRange
|
||||
): IntRange? = (this as ClosedRange<Int>).intersect(other as ClosedRange<Int>) ?.let {
|
||||
it.first .. it.second
|
||||
}
|
||||
|
||||
fun LongRange.intersect(
|
||||
other: LongRange
|
||||
): LongRange? = (this as ClosedRange<Long>).intersect(other as ClosedRange<Long>) ?.let {
|
||||
it.first .. it.second
|
||||
}
|
@@ -0,0 +1,54 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
/**
|
||||
* Executes the given [action] until getting of successful result specified number of [times].
|
||||
*
|
||||
* A zero-based index of current iteration is passed as a parameter to [action].
|
||||
*/
|
||||
inline fun <R> repeatOnFailure(
|
||||
onFailure: (Throwable) -> Boolean,
|
||||
action: () -> R
|
||||
): Result<R> {
|
||||
do {
|
||||
runCatching {
|
||||
action()
|
||||
}.onFailure {
|
||||
if (!onFailure(it)) {
|
||||
return Result.failure(it)
|
||||
}
|
||||
}.onSuccess {
|
||||
return Result.success(it)
|
||||
}
|
||||
} while (true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the given [action] until getting of successful result specified number of [times].
|
||||
*
|
||||
* A zero-based index of current iteration is passed as a parameter to [action].
|
||||
*/
|
||||
inline fun <R> repeatOnFailure(
|
||||
times: Int,
|
||||
onEachFailure: (Throwable) -> Unit = {},
|
||||
action: (Int) -> R
|
||||
): Optional<R> {
|
||||
var i = 0
|
||||
val result = repeatOnFailure(
|
||||
{
|
||||
onEachFailure(it)
|
||||
if (i < times) {
|
||||
i++
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
) {
|
||||
action(i)
|
||||
}
|
||||
return if (result.isSuccess) {
|
||||
Optional.presented(result.getOrThrow())
|
||||
} else {
|
||||
Optional.absent()
|
||||
}
|
||||
}
|
@@ -0,0 +1,6 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
val FixedSignsRange = 0 .. 100
|
||||
|
||||
expect fun Float.fixed(signs: Int): Float
|
||||
expect fun Double.fixed(signs: Int): Double
|
@@ -11,7 +11,7 @@ class DiffUtilsTests {
|
||||
val withIndex = oldList.withIndex()
|
||||
|
||||
for (count in 1 .. (floor(oldList.size.toFloat() / 2).toInt())) {
|
||||
for ((i, v) in withIndex) {
|
||||
for ((i, _) in withIndex) {
|
||||
if (i + count > oldList.lastIndex) {
|
||||
continue
|
||||
}
|
||||
@@ -32,7 +32,7 @@ class DiffUtilsTests {
|
||||
val withIndex = oldList.withIndex()
|
||||
|
||||
for (count in 1 .. (floor(oldList.size.toFloat() / 2).toInt())) {
|
||||
for ((i, v) in withIndex) {
|
||||
for ((i, _) in withIndex) {
|
||||
if (i + count > oldList.lastIndex) {
|
||||
continue
|
||||
}
|
||||
@@ -54,8 +54,8 @@ class DiffUtilsTests {
|
||||
val oldList = (0 until 10).map { it.toString() }
|
||||
val withIndex = oldList.withIndex()
|
||||
|
||||
for (step in 0 until oldList.size) {
|
||||
for ((i, v) in withIndex) {
|
||||
for (step in oldList.indices) {
|
||||
for ((i, _) in withIndex) {
|
||||
val mutable = oldList.toMutableList()
|
||||
val changes = (
|
||||
if (step == 0) i until oldList.size else (i until oldList.size step step)
|
||||
@@ -73,4 +73,83 @@ class DiffUtilsTests {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testThatSimpleRemoveApplyWorks() {
|
||||
val oldList = (0 until 10).toList()
|
||||
val withIndex = oldList.withIndex()
|
||||
|
||||
for (count in 1 .. (floor(oldList.size.toFloat() / 2).toInt())) {
|
||||
for ((i, _) in withIndex) {
|
||||
if (i + count > oldList.lastIndex) {
|
||||
continue
|
||||
}
|
||||
val removedSublist = oldList.subList(i, i + count)
|
||||
val mutableOldList = oldList.toMutableList()
|
||||
val targetList = oldList - removedSublist
|
||||
|
||||
mutableOldList.applyDiff(targetList)
|
||||
|
||||
assertEquals(
|
||||
targetList,
|
||||
mutableOldList
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testThatSimpleAddApplyWorks() {
|
||||
val oldList = (0 until 10).map { it.toString() }
|
||||
val withIndex = oldList.withIndex()
|
||||
|
||||
for (count in 1 .. (floor(oldList.size.toFloat() / 2).toInt())) {
|
||||
for ((i, _) in withIndex) {
|
||||
if (i + count > oldList.lastIndex) {
|
||||
continue
|
||||
}
|
||||
val addedSublist = oldList.subList(i, i + count).map { "added$it" }
|
||||
val mutable = oldList.toMutableList()
|
||||
mutable.addAll(i, addedSublist)
|
||||
val mutableOldList = oldList.toMutableList()
|
||||
|
||||
mutableOldList.applyDiff(mutable)
|
||||
|
||||
assertEquals(
|
||||
mutable,
|
||||
mutableOldList
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testThatSimpleChangesApplyWorks() {
|
||||
val oldList = (0 until 10).map { it.toString() }
|
||||
val withIndex = oldList.withIndex()
|
||||
|
||||
for (step in oldList.indices) {
|
||||
for ((i, _) in withIndex) {
|
||||
val mutable = oldList.toMutableList()
|
||||
|
||||
val newList = if (step == 0) {
|
||||
i until oldList.size
|
||||
} else {
|
||||
i until oldList.size step step
|
||||
}
|
||||
newList.forEach { index ->
|
||||
IndexedValue(index, mutable[index]) to IndexedValue(index, "changed$index").also {
|
||||
mutable[index] = it.value
|
||||
}
|
||||
}
|
||||
|
||||
val mutableOldList = oldList.toMutableList()
|
||||
mutableOldList.applyDiff(mutable)
|
||||
assertEquals(
|
||||
mutable,
|
||||
mutableOldList
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,61 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
import kotlinx.browser.document
|
||||
import org.w3c.dom.*
|
||||
|
||||
fun Node.onRemoved(block: () -> Unit): MutationObserver {
|
||||
lateinit var observer: MutationObserver
|
||||
|
||||
observer = MutationObserver { _, _ ->
|
||||
fun checkIfRemoved(node: Node): Boolean {
|
||||
return node.parentNode != document && (node.parentNode ?.let { checkIfRemoved(it) } ?: true)
|
||||
}
|
||||
|
||||
if (checkIfRemoved(this)) {
|
||||
observer.disconnect()
|
||||
block()
|
||||
}
|
||||
}
|
||||
|
||||
observer.observe(document, MutationObserverInit(childList = true, subtree = true))
|
||||
return observer
|
||||
}
|
||||
|
||||
fun Element.onVisibilityChanged(block: IntersectionObserverEntry.(Float, IntersectionObserver) -> Unit): IntersectionObserver {
|
||||
var previousIntersectionRatio = -1f
|
||||
val observer = IntersectionObserver { entries, observer ->
|
||||
entries.forEach {
|
||||
if (previousIntersectionRatio != it.intersectionRatio) {
|
||||
previousIntersectionRatio = it.intersectionRatio.toFloat()
|
||||
it.block(previousIntersectionRatio, observer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
observer.observe(this)
|
||||
return observer
|
||||
}
|
||||
|
||||
fun Element.onVisible(block: Element.(IntersectionObserver) -> Unit) {
|
||||
var previous = -1f
|
||||
onVisibilityChanged { intersectionRatio, observer ->
|
||||
if (previous != intersectionRatio) {
|
||||
if (intersectionRatio > 0 && previous == 0f) {
|
||||
block(observer)
|
||||
}
|
||||
previous = intersectionRatio
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Element.onInvisible(block: Element.(IntersectionObserver) -> Unit): IntersectionObserver {
|
||||
var previous = -1f
|
||||
return onVisibilityChanged { intersectionRatio, observer ->
|
||||
if (previous != intersectionRatio) {
|
||||
if (intersectionRatio == 0f && previous != 0f) {
|
||||
block(observer)
|
||||
}
|
||||
previous = intersectionRatio
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,124 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
import org.w3c.dom.DOMRectReadOnly
|
||||
import org.w3c.dom.Element
|
||||
|
||||
external interface IntersectionObserverOptions {
|
||||
/**
|
||||
* An Element or Document object which is an ancestor of the intended target, whose bounding rectangle will be
|
||||
* considered the viewport. Any part of the target not visible in the visible area of the root is not considered
|
||||
* visible.
|
||||
*/
|
||||
var root: Element?
|
||||
|
||||
/**
|
||||
* A string which specifies a set of offsets to add to the root's bounding_box when calculating intersections,
|
||||
* effectively shrinking or growing the root for calculation purposes. The syntax is approximately the same as that
|
||||
* for the CSS margin property; see The root element and root margin in Intersection Observer API for more
|
||||
* information on how the margin works and the syntax. The default is "0px 0px 0px 0px".
|
||||
*/
|
||||
var rootMargin: String?
|
||||
|
||||
/**
|
||||
* Either a single number or an array of numbers between 0.0 and 1.0, specifying a ratio of intersection area to
|
||||
* total bounding box area for the observed target. A value of 0.0 means that even a single visible pixel counts as
|
||||
* the target being visible. 1.0 means that the entire target element is visible. See Thresholds in Intersection
|
||||
* Observer API for a more in-depth description of how thresholds are used. The default is a threshold of 0.0.
|
||||
*/
|
||||
var threshold: Array<Number>?
|
||||
}
|
||||
fun IntersectionObserverOptions(
|
||||
block: IntersectionObserverOptions.() -> Unit = {}
|
||||
): IntersectionObserverOptions = js("{}").unsafeCast<IntersectionObserverOptions>().apply(block)
|
||||
|
||||
external interface IntersectionObserverEntry {
|
||||
/**
|
||||
* Returns the bounds rectangle of the target element as a DOMRectReadOnly. The bounds are computed as described in
|
||||
* the documentation for Element.getBoundingClientRect().
|
||||
*/
|
||||
val boundingClientRect: DOMRectReadOnly
|
||||
|
||||
/**
|
||||
* Returns the ratio of the intersectionRect to the boundingClientRect.
|
||||
*/
|
||||
val intersectionRatio: Number
|
||||
|
||||
/**
|
||||
* Returns a DOMRectReadOnly representing the target's visible area.
|
||||
*/
|
||||
val intersectionRect: DOMRectReadOnly
|
||||
|
||||
/**
|
||||
* A Boolean value which is true if the target element intersects with the intersection observer's root. If this is
|
||||
* true, then, the IntersectionObserverEntry describes a transition into a state of intersection; if it's false,
|
||||
* then you know the transition is from intersecting to not-intersecting.
|
||||
*/
|
||||
val isIntersecting: Boolean
|
||||
|
||||
/**
|
||||
* Returns a DOMRectReadOnly for the intersection observer's root.
|
||||
*/
|
||||
val rootBounds: DOMRectReadOnly
|
||||
|
||||
/**
|
||||
* The Element whose intersection with the root changed.
|
||||
*/
|
||||
val target: Element
|
||||
|
||||
/**
|
||||
* A DOMHighResTimeStamp indicating the time at which the intersection was recorded, relative to the
|
||||
* IntersectionObserver's time origin.
|
||||
*/
|
||||
val time: Double
|
||||
}
|
||||
|
||||
typealias IntersectionObserverCallback = (entries: Array<IntersectionObserverEntry>, observer: IntersectionObserver) -> Unit
|
||||
|
||||
/**
|
||||
* This is just an implementation from [this commentary](https://youtrack.jetbrains.com/issue/KT-43157#focus=Comments-27-4498582.0-0)
|
||||
* of Kotlin JS issue related to the absence of [Intersection Observer API](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver)
|
||||
*/
|
||||
external class IntersectionObserver(callback: IntersectionObserverCallback) {
|
||||
constructor(callback: IntersectionObserverCallback, options: IntersectionObserverOptions)
|
||||
|
||||
/**
|
||||
* The Element or Document whose bounds are used as the bounding box when testing for intersection. If no root value
|
||||
* was passed to the constructor or its value is null, the top-level document's viewport is used.
|
||||
*/
|
||||
val root: Element
|
||||
|
||||
/**
|
||||
* An offset rectangle applied to the root's bounding box when calculating intersections, effectively shrinking or
|
||||
* growing the root for calculation purposes. The value returned by this property may not be the same as the one
|
||||
* specified when calling the constructor as it may be changed to match internal requirements. Each offset can be
|
||||
* expressed in pixels (px) or as a percentage (%). The default is "0px 0px 0px 0px".
|
||||
*/
|
||||
val rootMargin: String
|
||||
|
||||
/**
|
||||
* A list of thresholds, sorted in increasing numeric order, where each threshold is a ratio of intersection area to
|
||||
* bounding box area of an observed target. Notifications for a target are generated when any of the thresholds are
|
||||
* crossed for that target. If no value was passed to the constructor, 0 is used.
|
||||
*/
|
||||
val thresholds: Array<Number>
|
||||
|
||||
/**
|
||||
* Stops the IntersectionObserver object from observing any target.
|
||||
*/
|
||||
fun disconnect()
|
||||
|
||||
/**
|
||||
* Tells the IntersectionObserver a target element to observe.
|
||||
*/
|
||||
fun observe(targetElement: Element)
|
||||
|
||||
/**
|
||||
* Returns an array of IntersectionObserverEntry objects for all observed targets.
|
||||
*/
|
||||
fun takeRecords(): Array<IntersectionObserverEntry>
|
||||
|
||||
/**
|
||||
* Tells the IntersectionObserver to stop observing a particular target element.
|
||||
*/
|
||||
fun unobserve(targetElement: Element)
|
||||
}
|
@@ -0,0 +1,12 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
import org.w3c.dom.Element
|
||||
|
||||
inline val Element.isOverflowWidth
|
||||
get() = scrollWidth > clientWidth
|
||||
|
||||
inline val Element.isOverflowHeight
|
||||
get() = scrollHeight > clientHeight
|
||||
|
||||
inline val Element.isOverflow
|
||||
get() = isOverflowHeight || isOverflowWidth
|
@@ -0,0 +1,54 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
import org.khronos.webgl.ArrayBuffer
|
||||
import org.w3c.dom.ErrorEvent
|
||||
import org.w3c.files.*
|
||||
import kotlin.js.Promise
|
||||
|
||||
/**
|
||||
* @suppress
|
||||
*/
|
||||
actual typealias MPPFile = File
|
||||
|
||||
fun MPPFile.readBytesPromise() = Promise<ByteArray> { success, failure ->
|
||||
val reader = FileReader()
|
||||
reader.onload = {
|
||||
success((reader.result as ArrayBuffer).toByteArray())
|
||||
Unit
|
||||
}
|
||||
reader.onerror = {
|
||||
failure(Exception((it as ErrorEvent).message))
|
||||
Unit
|
||||
}
|
||||
reader.readAsArrayBuffer(this)
|
||||
}
|
||||
|
||||
fun MPPFile.readBytes(): ByteArray {
|
||||
val reader = FileReaderSync()
|
||||
return reader.readAsArrayBuffer(this).toByteArray()
|
||||
}
|
||||
|
||||
private suspend fun MPPFile.dirtyReadBytes(): ByteArray = readBytesPromise().await()
|
||||
|
||||
/**
|
||||
* @suppress
|
||||
*/
|
||||
actual val MPPFile.filename: FileName
|
||||
get() = FileName(name)
|
||||
/**
|
||||
* @suppress
|
||||
*/
|
||||
actual val MPPFile.filesize: Long
|
||||
get() = size.toLong()
|
||||
/**
|
||||
* @suppress
|
||||
*/
|
||||
@Warning("That is not optimized version of bytes allocator. Use asyncBytesAllocator everywhere you can")
|
||||
actual val MPPFile.bytesAllocatorSync: ByteArrayAllocator
|
||||
get() = ::readBytes
|
||||
/**
|
||||
* @suppress
|
||||
*/
|
||||
@Warning("That is not optimized version of bytes allocator. Use asyncBytesAllocator everywhere you can")
|
||||
actual val MPPFile.bytesAllocator: SuspendByteArrayAllocator
|
||||
get() = ::dirtyReadBytes
|
@@ -0,0 +1,38 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
import kotlinx.browser.document
|
||||
import org.w3c.dom.*
|
||||
import org.w3c.dom.events.Event
|
||||
import org.w3c.dom.events.EventListener
|
||||
|
||||
fun Element.onActionOutside(type: String, options: dynamic = null, callback: (Event) -> Unit): EventListener {
|
||||
lateinit var observer: MutationObserver
|
||||
val listener = EventListener {
|
||||
val elementsToCheck = mutableListOf<Element>(this@onActionOutside)
|
||||
while (it.target != this@onActionOutside && elementsToCheck.isNotEmpty()) {
|
||||
val childrenGettingElement = elementsToCheck.removeFirst()
|
||||
for (i in 0 until childrenGettingElement.childElementCount) {
|
||||
elementsToCheck.add(childrenGettingElement.children[i] ?: continue)
|
||||
}
|
||||
}
|
||||
if (elementsToCheck.isEmpty()) {
|
||||
callback(it)
|
||||
}
|
||||
}
|
||||
if (options == null) {
|
||||
document.addEventListener(type, listener)
|
||||
} else {
|
||||
document.addEventListener(type, listener, options)
|
||||
}
|
||||
observer = onRemoved {
|
||||
if (options == null) {
|
||||
document.removeEventListener(type, listener)
|
||||
} else {
|
||||
document.removeEventListener(type, listener, options)
|
||||
}
|
||||
observer.disconnect()
|
||||
}
|
||||
return listener
|
||||
}
|
||||
|
||||
fun Element.onClickOutside(options: dynamic = null, callback: (Event) -> Unit) = onActionOutside("click", options, callback)
|
@@ -0,0 +1,8 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
import kotlinx.browser.window
|
||||
|
||||
fun openLink(link: String, target: String = "_blank", features: String = "") {
|
||||
window.open(link, target, features) ?.focus()
|
||||
}
|
||||
|
@@ -0,0 +1,8 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
import kotlin.coroutines.*
|
||||
import kotlin.js.Promise
|
||||
|
||||
suspend fun <T> Promise<T>.await(): T = suspendCoroutine { cont ->
|
||||
then({ cont.resume(it) }, { cont.resumeWithException(it) })
|
||||
}
|
@@ -0,0 +1,58 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
import org.w3c.dom.*
|
||||
import kotlin.js.Json
|
||||
import kotlin.js.json
|
||||
|
||||
external class ResizeObserver(
|
||||
callback: (Array<ResizeObserverEntry>, ResizeObserver) -> Unit
|
||||
) {
|
||||
fun observe(target: Element, options: Json = definedExternally)
|
||||
|
||||
fun unobserve(target: Element)
|
||||
|
||||
fun disconnect()
|
||||
}
|
||||
|
||||
external interface ResizeObserverSize {
|
||||
val blockSize: Float
|
||||
val inlineSize: Float
|
||||
}
|
||||
|
||||
external interface ResizeObserverEntry {
|
||||
val borderBoxSize: Array<ResizeObserverSize>
|
||||
val contentBoxSize: Array<ResizeObserverSize>
|
||||
val devicePixelContentBoxSize: Array<ResizeObserverSize>
|
||||
val contentRect: DOMRectReadOnly
|
||||
val target: Element
|
||||
}
|
||||
|
||||
fun ResizeObserver.observe(target: Element, options: ResizeObserverObserveOptions) = observe(
|
||||
target,
|
||||
json(
|
||||
"box" to options.box ?.name
|
||||
)
|
||||
)
|
||||
|
||||
class ResizeObserverObserveOptions(
|
||||
val box: Box? = null
|
||||
) {
|
||||
sealed interface Box {
|
||||
val name: String
|
||||
|
||||
object Content : Box {
|
||||
override val name: String
|
||||
get() = "content-box"
|
||||
}
|
||||
|
||||
object Border : Box {
|
||||
override val name: String
|
||||
get() = "border-box"
|
||||
}
|
||||
|
||||
object DevicePixelContent : Box {
|
||||
override val name: String
|
||||
get() = "device-pixel-content-box"
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,30 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
import kotlinx.browser.document
|
||||
import kotlinx.dom.createElement
|
||||
import org.w3c.dom.HTMLElement
|
||||
import org.w3c.dom.HTMLInputElement
|
||||
import org.w3c.files.get
|
||||
|
||||
fun selectFile(
|
||||
inputSetup: (HTMLInputElement) -> Unit = {},
|
||||
onFailure: (Throwable) -> Unit = {},
|
||||
onFile: (MPPFile) -> Unit
|
||||
) {
|
||||
(document.createElement("input") {
|
||||
(this as HTMLInputElement).apply {
|
||||
type = "file"
|
||||
onchange = {
|
||||
runCatching {
|
||||
files ?.get(0) ?: error("File must not be null")
|
||||
}.onSuccess {
|
||||
onFile(it)
|
||||
}.onFailure {
|
||||
onFailure(it)
|
||||
}
|
||||
}
|
||||
inputSetup(this)
|
||||
}
|
||||
} as HTMLElement).click()
|
||||
}
|
||||
|
@@ -0,0 +1,14 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
import kotlinx.browser.document
|
||||
import org.w3c.dom.HTMLAnchorElement
|
||||
|
||||
fun triggerDownloadFile(filename: String, fileLink: String) {
|
||||
val hiddenElement = document.createElement("a") as HTMLAnchorElement
|
||||
|
||||
hiddenElement.href = fileLink
|
||||
hiddenElement.target = "_blank"
|
||||
hiddenElement.download = filename
|
||||
hiddenElement.click()
|
||||
}
|
||||
|
@@ -0,0 +1,4 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
actual fun Float.fixed(signs: Int): Float = this.asDynamic().toFixed(signs.coerceIn(FixedSignsRange)).unsafeCast<String>().toFloat()
|
||||
actual fun Double.fixed(signs: Int): Double = this.asDynamic().toFixed(signs.coerceIn(FixedSignsRange)).unsafeCast<String>().toDouble()
|
@@ -0,0 +1,20 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.util.UUID
|
||||
|
||||
fun InputStream.downloadToTempFile(
|
||||
fileName: String = UUID.randomUUID().toString(),
|
||||
fileExtension: String? = ".temp",
|
||||
folder: File? = null
|
||||
) = File.createTempFile(
|
||||
fileName,
|
||||
fileExtension,
|
||||
folder
|
||||
).apply {
|
||||
outputStream().use {
|
||||
copyTo(it)
|
||||
}
|
||||
deleteOnExit()
|
||||
}
|
@@ -0,0 +1,37 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
import dev.inmo.micro_utils.coroutines.doInIO
|
||||
import dev.inmo.micro_utils.coroutines.doOutsideOfCoroutine
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* @suppress
|
||||
*/
|
||||
actual typealias MPPFile = File
|
||||
|
||||
/**
|
||||
* @suppress
|
||||
*/
|
||||
actual val MPPFile.filename: FileName
|
||||
get() = FileName(name)
|
||||
/**
|
||||
* @suppress
|
||||
*/
|
||||
actual val MPPFile.filesize: Long
|
||||
get() = length()
|
||||
/**
|
||||
* @suppress
|
||||
*/
|
||||
actual val MPPFile.bytesAllocatorSync: ByteArrayAllocator
|
||||
get() = ::readBytes
|
||||
/**
|
||||
* @suppress
|
||||
*/
|
||||
actual val MPPFile.bytesAllocator: SuspendByteArrayAllocator
|
||||
get() = {
|
||||
doInIO {
|
||||
doOutsideOfCoroutine {
|
||||
readBytes()
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,12 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
|
||||
actual fun Float.fixed(signs: Int): Float = BigDecimal.valueOf(this.toDouble())
|
||||
.setScale(signs.coerceIn(FixedSignsRange), RoundingMode.HALF_UP)
|
||||
.toFloat();
|
||||
|
||||
actual fun Double.fixed(signs: Int): Double = BigDecimal.valueOf(this)
|
||||
.setScale(signs.coerceIn(FixedSignsRange), RoundingMode.HALF_UP)
|
||||
.toDouble();
|
36
common/src/linuxX64Main/kotlin/ActualMPPFile.kt
Normal file
36
common/src/linuxX64Main/kotlin/ActualMPPFile.kt
Normal file
@@ -0,0 +1,36 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
import okio.FileSystem
|
||||
import okio.Path
|
||||
import okio.use
|
||||
|
||||
actual typealias MPPFile = Path
|
||||
|
||||
/**
|
||||
* @suppress
|
||||
*/
|
||||
actual val MPPFile.filename: FileName
|
||||
get() = FileName(toString())
|
||||
/**
|
||||
* @suppress
|
||||
*/
|
||||
actual val MPPFile.filesize: Long
|
||||
get() = FileSystem.SYSTEM.openReadOnly(this).use {
|
||||
it.size()
|
||||
}
|
||||
/**
|
||||
* @suppress
|
||||
*/
|
||||
actual val MPPFile.bytesAllocatorSync: ByteArrayAllocator
|
||||
get() = {
|
||||
FileSystem.SYSTEM.read(this) {
|
||||
readByteArray()
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @suppress
|
||||
*/
|
||||
actual val MPPFile.bytesAllocator: SuspendByteArrayAllocator
|
||||
get() = {
|
||||
bytesAllocatorSync()
|
||||
}
|
26
common/src/linuxX64Main/kotlin/fixed.kt
Normal file
26
common/src/linuxX64Main/kotlin/fixed.kt
Normal file
@@ -0,0 +1,26 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
import kotlinx.cinterop.ByteVar
|
||||
import kotlinx.cinterop.allocArray
|
||||
import kotlinx.cinterop.memScoped
|
||||
import kotlinx.cinterop.toKString
|
||||
import platform.posix.snprintf
|
||||
import platform.posix.sprintf
|
||||
|
||||
actual fun Float.fixed(signs: Int): Float {
|
||||
return memScoped {
|
||||
val buff = allocArray<ByteVar>(Float.SIZE_BYTES * 2)
|
||||
|
||||
sprintf(buff, "%.${signs}f", this@fixed)
|
||||
buff.toKString().toFloat()
|
||||
}
|
||||
}
|
||||
|
||||
actual fun Double.fixed(signs: Int): Double {
|
||||
return memScoped {
|
||||
val buff = allocArray<ByteVar>(Double.SIZE_BYTES * 2)
|
||||
|
||||
sprintf(buff, "%.${signs}f", this@fixed)
|
||||
buff.toKString().toDouble()
|
||||
}
|
||||
}
|
@@ -0,0 +1,76 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import androidx.fragment.app.Fragment
|
||||
import java.io.Serializable
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
object ArgumentPropertyNullableDelegate {
|
||||
operator fun <T: Any> getValue(thisRef: Fragment, property: KProperty<*>): T? {
|
||||
val arguments = thisRef.arguments ?: return null
|
||||
val key = property.name
|
||||
return when (property.getter.returnType.classifier) {
|
||||
// Scalars
|
||||
String::class -> arguments.getString(key)
|
||||
Boolean::class -> arguments.getBoolean(key)
|
||||
Byte::class -> arguments.getByte(key)
|
||||
Char::class -> arguments.getChar(key)
|
||||
Double::class -> arguments.getDouble(key)
|
||||
Float::class -> arguments.getFloat(key)
|
||||
Int::class -> arguments.getInt(key)
|
||||
Long::class -> arguments.getLong(key)
|
||||
Short::class -> arguments.getShort(key)
|
||||
|
||||
// References
|
||||
Bundle::class -> arguments.getBundle(key)
|
||||
CharSequence::class -> arguments.getCharSequence(key)
|
||||
Parcelable::class -> arguments.getParcelable(key)
|
||||
|
||||
// Scalar arrays
|
||||
BooleanArray::class -> arguments.getBooleanArray(key)
|
||||
ByteArray::class -> arguments.getByteArray(key)
|
||||
CharArray::class -> arguments.getCharArray(key)
|
||||
DoubleArray::class -> arguments.getDoubleArray(key)
|
||||
FloatArray::class -> arguments.getFloatArray(key)
|
||||
IntArray::class -> arguments.getIntArray(key)
|
||||
LongArray::class -> arguments.getLongArray(key)
|
||||
ShortArray::class -> arguments.getShortArray(key)
|
||||
Array::class -> {
|
||||
val componentType = property.returnType.classifier ?.javaClass ?.componentType!!
|
||||
@Suppress("UNCHECKED_CAST") // Checked by reflection.
|
||||
when {
|
||||
Parcelable::class.java.isAssignableFrom(componentType) -> {
|
||||
arguments.getParcelableArray(key)
|
||||
}
|
||||
String::class.java.isAssignableFrom(componentType) -> {
|
||||
arguments.getStringArray(key)
|
||||
}
|
||||
CharSequence::class.java.isAssignableFrom(componentType) -> {
|
||||
arguments.getCharSequenceArray(key)
|
||||
}
|
||||
Serializable::class.java.isAssignableFrom(componentType) -> {
|
||||
arguments.getSerializable(key)
|
||||
}
|
||||
else -> {
|
||||
val valueType = componentType.canonicalName
|
||||
throw IllegalArgumentException(
|
||||
"Illegal value array type $valueType for key \"$key\""
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Serializable::class -> arguments.getSerializable(key)
|
||||
else -> null
|
||||
} as? T
|
||||
}
|
||||
}
|
||||
|
||||
object ArgumentPropertyNonNullableDelegate {
|
||||
operator fun <T: Any> getValue(thisRef: Fragment, property: KProperty<*>): T {
|
||||
return ArgumentPropertyNullableDelegate.getValue<T>(thisRef, property)!!
|
||||
}
|
||||
}
|
||||
|
||||
fun argumentOrNull() = ArgumentPropertyNullableDelegate
|
||||
fun argumentOrThrow() = ArgumentPropertyNonNullableDelegate
|
@@ -0,0 +1,61 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
import android.app.Activity
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.children
|
||||
import androidx.fragment.app.Fragment
|
||||
|
||||
fun findViewsByTag(viewGroup: ViewGroup, tag: Any?): List<View> {
|
||||
return viewGroup.children.flatMap {
|
||||
findViewsByTag(it, tag)
|
||||
}.toList()
|
||||
}
|
||||
|
||||
fun findViewsByTag(viewGroup: ViewGroup, key: Int, tag: Any?): List<View> {
|
||||
return viewGroup.children.flatMap {
|
||||
findViewsByTag(it, key, tag)
|
||||
}.toList()
|
||||
}
|
||||
|
||||
fun findViewsByTag(view: View, tag: Any?): List<View> {
|
||||
val result = mutableListOf<View>()
|
||||
if (view.tag == tag) {
|
||||
result.add(view)
|
||||
}
|
||||
if (view is ViewGroup) {
|
||||
result.addAll(findViewsByTag(view, tag))
|
||||
}
|
||||
return result.toList()
|
||||
}
|
||||
|
||||
fun findViewsByTag(view: View, key: Int, tag: Any?): List<View> {
|
||||
val result = mutableListOf<View>()
|
||||
if (view.getTag(key) == tag) {
|
||||
result.add(view)
|
||||
}
|
||||
if (view is ViewGroup) {
|
||||
result.addAll(findViewsByTag(view, key, tag))
|
||||
}
|
||||
return result.toList()
|
||||
}
|
||||
|
||||
fun Activity.findViewsByTag(tag: Any?) = rootView ?.let {
|
||||
findViewsByTag(it, tag)
|
||||
}
|
||||
|
||||
fun Activity.findViewsByTag(key: Int, tag: Any?) = rootView ?.let {
|
||||
findViewsByTag(it, key, tag)
|
||||
}
|
||||
|
||||
fun Fragment.findViewsByTag(tag: Any?) = view ?.let {
|
||||
findViewsByTag(it, tag)
|
||||
}
|
||||
|
||||
fun Fragment.findViewsByTag(key: Int, tag: Any?) = view ?.let {
|
||||
findViewsByTag(it, key, tag)
|
||||
}
|
||||
|
||||
fun Fragment.findViewsByTagInActivity(tag: Any?) = activity ?.findViewsByTag(tag)
|
||||
|
||||
fun Fragment.findViewsByTagInActivity(key: Int, tag: Any?) = activity ?.findViewsByTag(key, tag)
|
@@ -0,0 +1,7 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
import android.app.Activity
|
||||
import android.view.View
|
||||
|
||||
val Activity.rootView: View?
|
||||
get() = findViewById<View?>(android.R.id.content) ?.rootView ?: window.decorView.findViewById<View?>(android.R.id.content) ?.rootView
|
@@ -36,12 +36,12 @@ fun View.toggleVisibility(goneOnHide: Boolean = true) {
|
||||
|
||||
fun View.changeVisibility(show: Boolean = !isShown, goneOnHide: Boolean = true) {
|
||||
if (show) {
|
||||
show()
|
||||
} else {
|
||||
if (goneOnHide) {
|
||||
gone()
|
||||
} else {
|
||||
hide()
|
||||
}
|
||||
} else {
|
||||
show()
|
||||
}
|
||||
}
|
||||
|
36
common/src/mingwX64Main/kotlin/ActualMPPFile.kt
Normal file
36
common/src/mingwX64Main/kotlin/ActualMPPFile.kt
Normal file
@@ -0,0 +1,36 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
import okio.FileSystem
|
||||
import okio.Path
|
||||
import okio.use
|
||||
|
||||
actual typealias MPPFile = Path
|
||||
|
||||
/**
|
||||
* @suppress
|
||||
*/
|
||||
actual val MPPFile.filename: FileName
|
||||
get() = FileName(toString())
|
||||
/**
|
||||
* @suppress
|
||||
*/
|
||||
actual val MPPFile.filesize: Long
|
||||
get() = FileSystem.SYSTEM.openReadOnly(this).use {
|
||||
it.size()
|
||||
}
|
||||
/**
|
||||
* @suppress
|
||||
*/
|
||||
actual val MPPFile.bytesAllocatorSync: ByteArrayAllocator
|
||||
get() = {
|
||||
FileSystem.SYSTEM.read(this) {
|
||||
readByteArray()
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @suppress
|
||||
*/
|
||||
actual val MPPFile.bytesAllocator: SuspendByteArrayAllocator
|
||||
get() = {
|
||||
bytesAllocatorSync()
|
||||
}
|
26
common/src/mingwX64Main/kotlin/fixed.kt
Normal file
26
common/src/mingwX64Main/kotlin/fixed.kt
Normal file
@@ -0,0 +1,26 @@
|
||||
package dev.inmo.micro_utils.common
|
||||
|
||||
import kotlinx.cinterop.ByteVar
|
||||
import kotlinx.cinterop.allocArray
|
||||
import kotlinx.cinterop.memScoped
|
||||
import kotlinx.cinterop.toKString
|
||||
import platform.posix.snprintf
|
||||
import platform.posix.sprintf
|
||||
|
||||
actual fun Float.fixed(signs: Int): Float {
|
||||
return memScoped {
|
||||
val buff = allocArray<ByteVar>(Float.SIZE_BYTES * 2)
|
||||
|
||||
sprintf(buff, "%.${signs}f", this@fixed)
|
||||
buff.toKString().toFloat()
|
||||
}
|
||||
}
|
||||
|
||||
actual fun Double.fixed(signs: Int): Double {
|
||||
return memScoped {
|
||||
val buff = allocArray<ByteVar>(Double.SIZE_BYTES * 2)
|
||||
|
||||
sprintf(buff, "%.${signs}f", this@fixed)
|
||||
buff.toKString().toDouble()
|
||||
}
|
||||
}
|
@@ -10,13 +10,19 @@ kotlin {
|
||||
sourceSets {
|
||||
commonMain {
|
||||
dependencies {
|
||||
api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version"
|
||||
api libs.kt.coroutines
|
||||
}
|
||||
}
|
||||
jsMain {
|
||||
dependencies {
|
||||
api project(":micro_utils.common")
|
||||
}
|
||||
}
|
||||
androidMain {
|
||||
dependencies {
|
||||
api "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version"
|
||||
}
|
||||
api libs.kt.coroutines.android
|
||||
}
|
||||
dependsOn(jvmMain)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
20
coroutines/compose/build.gradle
Normal file
20
coroutines/compose/build.gradle
Normal file
@@ -0,0 +1,20 @@
|
||||
plugins {
|
||||
id "org.jetbrains.kotlin.multiplatform"
|
||||
id "org.jetbrains.kotlin.plugin.serialization"
|
||||
id "com.android.library"
|
||||
alias(libs.plugins.jb.compose)
|
||||
}
|
||||
|
||||
apply from: "$mppProjectWithSerializationAndComposePresetPath"
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
commonMain {
|
||||
dependencies {
|
||||
api libs.kt.coroutines
|
||||
api project(":micro_utils.coroutines")
|
||||
api project(":micro_utils.common.compose")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,60 @@
|
||||
package dev.inmo.micro_utils.coroutines.compose
|
||||
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||
import dev.inmo.micro_utils.common.applyDiff
|
||||
import dev.inmo.micro_utils.coroutines.ExceptionHandler
|
||||
import dev.inmo.micro_utils.coroutines.defaultSafelyWithoutExceptionHandlerWithNull
|
||||
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
/**
|
||||
* Each value of [this] [Flow] will trigger [applyDiff] to the result [SnapshotStateList]
|
||||
*
|
||||
* @param scope Will be used to [subscribeSafelyWithoutExceptions] on [this] to update returned [SnapshotStateList]
|
||||
* @param useContextOnChange Will be used to change context inside of [subscribeSafelyWithoutExceptions] to ensure that
|
||||
* change will happen in the required [CoroutineContext]. [Dispatchers.Main] by default
|
||||
* @param onException Will be passed to the [subscribeSafelyWithoutExceptions] as uncaught exceptions handler
|
||||
*/
|
||||
@Suppress("NOTHING_TO_INLINE")
|
||||
inline fun <reified T> Flow<List<T>>.asMutableComposeListState(
|
||||
scope: CoroutineScope,
|
||||
useContextOnChange: CoroutineContext? = Dispatchers.Main,
|
||||
noinline onException: ExceptionHandler<List<T>?> = defaultSafelyWithoutExceptionHandlerWithNull,
|
||||
): SnapshotStateList<T> {
|
||||
val state = mutableStateListOf<T>()
|
||||
val changeBlock: suspend (List<T>) -> Unit = useContextOnChange ?.let {
|
||||
{
|
||||
withContext(useContextOnChange) {
|
||||
state.applyDiff(it)
|
||||
}
|
||||
}
|
||||
} ?: {
|
||||
state.applyDiff(it)
|
||||
}
|
||||
subscribeSafelyWithoutExceptions(scope, onException, changeBlock)
|
||||
return state
|
||||
}
|
||||
|
||||
/**
|
||||
* In fact, it is just classcast of [asMutableComposeListState] to [List]
|
||||
*
|
||||
* @param scope Will be used to [subscribeSafelyWithoutExceptions] on [this] to update returned [List]
|
||||
* @param useContextOnChange Will be used to change context inside of [subscribeSafelyWithoutExceptions] to ensure that
|
||||
* change will happen in the required [CoroutineContext]. [Dispatchers.Main] by default
|
||||
* @param onException Will be passed to the [subscribeSafelyWithoutExceptions] as uncaught exceptions handler
|
||||
*
|
||||
* @return Changing in time [List] which follow [Flow] values
|
||||
*/
|
||||
@Suppress("NOTHING_TO_INLINE")
|
||||
inline fun <reified T> Flow<List<T>>.asComposeList(
|
||||
scope: CoroutineScope,
|
||||
useContextOnChange: CoroutineContext? = Dispatchers.Main,
|
||||
noinline onException: ExceptionHandler<List<T>?> = defaultSafelyWithoutExceptionHandlerWithNull,
|
||||
): List<T> = asMutableComposeListState(scope, useContextOnChange, onException)
|
||||
|
@@ -0,0 +1,94 @@
|
||||
package dev.inmo.micro_utils.coroutines.compose
|
||||
|
||||
import androidx.compose.runtime.*
|
||||
import dev.inmo.micro_utils.common.compose.asState
|
||||
import dev.inmo.micro_utils.coroutines.ExceptionHandler
|
||||
import dev.inmo.micro_utils.coroutines.defaultSafelyWithoutExceptionHandlerWithNull
|
||||
import dev.inmo.micro_utils.coroutines.doInUI
|
||||
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
/**
|
||||
* Will map [this] [Flow] as [MutableState]. Returned [MutableState] WILL NOT change source [Flow]
|
||||
*
|
||||
* @param initial First value which will be passed to the result [MutableState]
|
||||
* @param scope Will be used to [subscribeSafelyWithoutExceptions] on [this] to update returned [MutableState]
|
||||
* @param useContextOnChange Will be used to change context inside of [subscribeSafelyWithoutExceptions] to ensure that
|
||||
* change will happen in the required [CoroutineContext]. [Dispatchers.Main] by default
|
||||
* @param onException Will be passed to the [subscribeSafelyWithoutExceptions] as uncaught exceptions handler
|
||||
*/
|
||||
fun <T> Flow<T>.asMutableComposeState(
|
||||
initial: T,
|
||||
scope: CoroutineScope,
|
||||
useContextOnChange: CoroutineContext? = Dispatchers.Main,
|
||||
onException: ExceptionHandler<T?> = defaultSafelyWithoutExceptionHandlerWithNull,
|
||||
): MutableState<T> {
|
||||
val state = mutableStateOf(initial)
|
||||
val changeBlock: suspend (T) -> Unit = useContextOnChange ?.let {
|
||||
{
|
||||
withContext(useContextOnChange) {
|
||||
state.value = it
|
||||
}
|
||||
}
|
||||
} ?: {
|
||||
state.value = it
|
||||
}
|
||||
subscribeSafelyWithoutExceptions(scope, onException, block = changeBlock)
|
||||
return state
|
||||
}
|
||||
|
||||
/**
|
||||
* Will map [this] [StateFlow] as [MutableState]. Returned [MutableState] WILL NOT change source [StateFlow].
|
||||
* This conversation will pass its [StateFlow.value] as the first value
|
||||
*
|
||||
* @param scope Will be used to [subscribeSafelyWithoutExceptions] on [this] to update returned [MutableState]
|
||||
* @param useContextOnChange Will be used to change context inside of [subscribeSafelyWithoutExceptions] to ensure that
|
||||
* change will happen in the required [CoroutineContext]. [Dispatchers.Main] by default
|
||||
* @param onException Will be passed to the [subscribeSafelyWithoutExceptions] as uncaught exceptions handler
|
||||
*/
|
||||
@Suppress("NOTHING_TO_INLINE")
|
||||
inline fun <T> StateFlow<T>.asMutableComposeState(
|
||||
scope: CoroutineScope,
|
||||
useContextOnChange: CoroutineContext? = Dispatchers.Main,
|
||||
noinline onException: ExceptionHandler<T?> = defaultSafelyWithoutExceptionHandlerWithNull,
|
||||
): MutableState<T> = asMutableComposeState(value, scope, useContextOnChange, onException)
|
||||
|
||||
/**
|
||||
* Will create [MutableState] using [asMutableComposeState] and use [asState] to convert it as immutable state
|
||||
*
|
||||
* @param initial First value which will be passed to the result [State]
|
||||
* @param scope Will be used to [subscribeSafelyWithoutExceptions] on [this] to update returned [State]
|
||||
* @param useContextOnChange Will be used to change context inside of [subscribeSafelyWithoutExceptions] to ensure that
|
||||
* change will happen in the required [CoroutineContext]. [Dispatchers.Main] by default
|
||||
* @param onException Will be passed to the [subscribeSafelyWithoutExceptions] as uncaught exceptions handler
|
||||
*/
|
||||
fun <T> Flow<T>.asComposeState(
|
||||
initial: T,
|
||||
scope: CoroutineScope,
|
||||
useContextOnChange: CoroutineContext? = Dispatchers.Main,
|
||||
onException: ExceptionHandler<T?> = defaultSafelyWithoutExceptionHandlerWithNull,
|
||||
): State<T> {
|
||||
val state = asMutableComposeState(initial, scope, useContextOnChange, onException)
|
||||
return state.asState()
|
||||
}
|
||||
|
||||
/**
|
||||
* Will map [this] [StateFlow] as [State]. This conversation will pass its [StateFlow.value] as the first value
|
||||
*
|
||||
* @param scope Will be used to [subscribeSafelyWithoutExceptions] on [this] to update returned [State]
|
||||
* @param useContextOnChange Will be used to change context inside of [subscribeSafelyWithoutExceptions] to ensure that
|
||||
* change will happen in the required [CoroutineContext]. [Dispatchers.Main] by default
|
||||
* @param onException Will be passed to the [subscribeSafelyWithoutExceptions] as uncaught exceptions handler
|
||||
*/
|
||||
@Suppress("NOTHING_TO_INLINE")
|
||||
inline fun <T> StateFlow<T>.asComposeState(
|
||||
scope: CoroutineScope,
|
||||
useContextOnChange: CoroutineContext? = Dispatchers.Main,
|
||||
noinline onException: ExceptionHandler<T?> = defaultSafelyWithoutExceptionHandlerWithNull,
|
||||
): State<T> = asComposeState(value, scope, useContextOnChange, onException)
|
||||
|
@@ -0,0 +1,14 @@
|
||||
package dev.inmo.micro_utils.coroutines.compose
|
||||
|
||||
import androidx.compose.runtime.*
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.job
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
fun Composition.linkWithJob(job: Job) {
|
||||
job.invokeOnCompletion {
|
||||
this@linkWithJob.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
fun Composition.linkWithContext(coroutineContext: CoroutineContext) = linkWithJob(coroutineContext.job)
|
@@ -0,0 +1,26 @@
|
||||
package dev.inmo.micro_utils.coroutines.compose
|
||||
|
||||
import androidx.compose.runtime.*
|
||||
import dev.inmo.micro_utils.common.compose.linkWithElement
|
||||
import kotlinx.coroutines.*
|
||||
import org.jetbrains.compose.web.dom.DOMScope
|
||||
import org.w3c.dom.Element
|
||||
|
||||
suspend fun <TElement : Element> renderComposableAndLinkToContext(
|
||||
root: TElement,
|
||||
monotonicFrameClock: MonotonicFrameClock = DefaultMonotonicFrameClock,
|
||||
content: @Composable DOMScope<TElement>.() -> Unit
|
||||
): Composition = org.jetbrains.compose.web.renderComposable(root, monotonicFrameClock, content).apply {
|
||||
linkWithContext(
|
||||
currentCoroutineContext()
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun <TElement : Element> renderComposableAndLinkToContextAndRoot(
|
||||
root: TElement,
|
||||
monotonicFrameClock: MonotonicFrameClock = DefaultMonotonicFrameClock,
|
||||
content: @Composable DOMScope<TElement>.() -> Unit
|
||||
): Composition = org.jetbrains.compose.web.renderComposable(root, monotonicFrameClock, content).apply {
|
||||
linkWithContext(currentCoroutineContext())
|
||||
linkWithElement(root)
|
||||
}
|
1
coroutines/compose/src/main/AndroidManifest.xml
Normal file
1
coroutines/compose/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
||||
<manifest package="dev.inmo.micro_utils.coroutines.compose"/>
|
@@ -0,0 +1,100 @@
|
||||
package dev.inmo.micro_utils.coroutines
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
|
||||
private sealed interface AccumulatorFlowStep<T>
|
||||
private data class DataRetrievedAccumulatorFlowStep<T>(val data: T) : AccumulatorFlowStep<T>
|
||||
private data class SubscribeAccumulatorFlowStep<T>(val channel: Channel<T>) : AccumulatorFlowStep<T>
|
||||
private data class UnsubscribeAccumulatorFlowStep<T>(val channel: Channel<T>) : AccumulatorFlowStep<T>
|
||||
|
||||
/**
|
||||
* This [Flow] will have behaviour very similar to [SharedFlow], but there are several differences:
|
||||
*
|
||||
* * All unhandled by [FlowCollector] data will not be removed from [AccumulatorFlow] and will be sent to new
|
||||
* [FlowCollector]s until anybody will handle it
|
||||
* * Here there are an [activeData] where data [T] will be stored until somebody will handle it
|
||||
*/
|
||||
class AccumulatorFlow<T>(
|
||||
sourceDataFlow: Flow<T>,
|
||||
scope: CoroutineScope
|
||||
) : AbstractFlow<T>() {
|
||||
private val subscope = scope.LinkedSupervisorScope()
|
||||
private val activeData = ArrayDeque<T>()
|
||||
private val dataMutex = Mutex()
|
||||
private val channelsForBroadcast = mutableListOf<Channel<T>>()
|
||||
private val channelsMutex = Mutex()
|
||||
private val steps = subscope.actor<AccumulatorFlowStep<T>> { step ->
|
||||
when (step) {
|
||||
is DataRetrievedAccumulatorFlowStep -> {
|
||||
if (activeData.firstOrNull() === step.data) {
|
||||
dataMutex.withLock {
|
||||
activeData.removeFirst()
|
||||
}
|
||||
}
|
||||
}
|
||||
is SubscribeAccumulatorFlowStep -> channelsMutex.withLock {
|
||||
channelsForBroadcast.add(step.channel)
|
||||
dataMutex.withLock {
|
||||
val dataToSend = activeData.toList()
|
||||
safelyWithoutExceptions {
|
||||
dataToSend.forEach { step.channel.send(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
is UnsubscribeAccumulatorFlowStep -> channelsMutex.withLock {
|
||||
channelsForBroadcast.remove(step.channel)
|
||||
}
|
||||
}
|
||||
}
|
||||
private val subscriptionJob = sourceDataFlow.subscribeSafelyWithoutExceptions(subscope) {
|
||||
dataMutex.withLock {
|
||||
activeData.addLast(it)
|
||||
}
|
||||
channelsMutex.withLock {
|
||||
channelsForBroadcast.forEach { channel ->
|
||||
safelyWithResult {
|
||||
channel.send(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun collectSafely(collector: FlowCollector<T>) {
|
||||
val channel = Channel<T>(Channel.UNLIMITED, BufferOverflow.SUSPEND)
|
||||
steps.send(SubscribeAccumulatorFlowStep(channel))
|
||||
val result = runCatchingSafely {
|
||||
for (data in channel) {
|
||||
val emitResult = runCatchingSafely {
|
||||
collector.emit(data)
|
||||
}
|
||||
if (emitResult.isSuccess || emitResult.exceptionOrNull() is CancellationException) {
|
||||
steps.send(DataRetrievedAccumulatorFlowStep(data))
|
||||
}
|
||||
emitResult.getOrThrow()
|
||||
}
|
||||
}
|
||||
channel.cancel()
|
||||
steps.send(UnsubscribeAccumulatorFlowStep(channel))
|
||||
result.getOrThrow()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates [AccumulatorFlow] using [this] as base [Flow]
|
||||
*/
|
||||
fun <T> Flow<T>.accumulatorFlow(scope: CoroutineScope): Flow<T> {
|
||||
return AccumulatorFlow(this, scope)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates [AccumulatorFlow] using [this] with [receiveAsFlow] to get
|
||||
*/
|
||||
fun <T> Channel<T>.accumulatorFlow(scope: CoroutineScope): Flow<T> {
|
||||
return receiveAsFlow().accumulatorFlow(scope)
|
||||
}
|
@@ -1,19 +1,15 @@
|
||||
package dev.inmo.micro_utils.coroutines
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.flow.consumeAsFlow
|
||||
|
||||
fun <T> CoroutineScope.actor(
|
||||
channelCapacity: Int = Channel.UNLIMITED,
|
||||
block: suspend (T) -> Unit
|
||||
): Channel<T> {
|
||||
val channel = Channel<T>(channelCapacity)
|
||||
launch {
|
||||
for (data in channel) {
|
||||
block(data)
|
||||
}
|
||||
}
|
||||
channel.consumeAsFlow().subscribe(this, block)
|
||||
return channel
|
||||
}
|
||||
|
||||
|
@@ -0,0 +1,30 @@
|
||||
package dev.inmo.micro_utils.coroutines
|
||||
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.consumeAsFlow
|
||||
|
||||
fun <T> CoroutineScope.actorAsync(
|
||||
channelCapacity: Int = Channel.UNLIMITED,
|
||||
markerFactory: suspend (T) -> Any? = { null },
|
||||
block: suspend (T) -> Unit
|
||||
): Channel<T> {
|
||||
val channel = Channel<T>(channelCapacity)
|
||||
channel.consumeAsFlow().subscribeAsync(this, markerFactory, block)
|
||||
return channel
|
||||
}
|
||||
|
||||
inline fun <T> CoroutineScope.safeActorAsync(
|
||||
channelCapacity: Int = Channel.UNLIMITED,
|
||||
noinline onException: ExceptionHandler<Unit> = defaultSafelyExceptionHandler,
|
||||
noinline markerFactory: suspend (T) -> Any? = { null },
|
||||
crossinline block: suspend (T) -> Unit
|
||||
): Channel<T> = actorAsync(
|
||||
channelCapacity,
|
||||
markerFactory
|
||||
) {
|
||||
safely(onException) {
|
||||
block(it)
|
||||
}
|
||||
}
|
||||
|
@@ -6,23 +6,19 @@ import kotlin.coroutines.*
|
||||
suspend fun <T> Iterable<Deferred<T>>.awaitFirstWithDeferred(
|
||||
scope: CoroutineScope,
|
||||
cancelOnResult: Boolean = true
|
||||
): Pair<Deferred<T>, T> = suspendCoroutine<Pair<Deferred<T>, T>> { continuation ->
|
||||
scope.launch(SupervisorJob()) {
|
||||
val scope = this
|
||||
): Pair<Deferred<T>, T> {
|
||||
val resultDeferred = CompletableDeferred<Pair<Deferred<T>, T>>()
|
||||
val scope = scope.LinkedSupervisorScope()
|
||||
forEach {
|
||||
scope.launch {
|
||||
continuation.resume(it to it.await())
|
||||
resultDeferred.complete(it to it.await())
|
||||
scope.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
}.also {
|
||||
return resultDeferred.await().also {
|
||||
if (cancelOnResult) {
|
||||
forEach {
|
||||
try {
|
||||
it.cancel()
|
||||
} catch (e: IllegalStateException) {
|
||||
e.printStackTrace()
|
||||
runCatchingSafely { it.cancel() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,39 @@
|
||||
package dev.inmo.micro_utils.coroutines
|
||||
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlin.js.JsName
|
||||
import kotlin.jvm.JvmName
|
||||
|
||||
inline fun <T, R> Flow<Flow<T>>.flatMap(
|
||||
crossinline mapper: suspend (T) -> R
|
||||
) = flow {
|
||||
collect {
|
||||
it.collect {
|
||||
emit(mapper(it))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JsName("flatMapIterable")
|
||||
@JvmName("flatMapIterable")
|
||||
inline fun <T, R> Flow<Iterable<T>>.flatMap(
|
||||
crossinline mapper: suspend (T) -> R
|
||||
) = map {
|
||||
it.asFlow()
|
||||
}.flatMap(mapper)
|
||||
|
||||
inline fun <T, R> Flow<Flow<T>>.flatMapNotNull(
|
||||
crossinline mapper: suspend (T) -> R
|
||||
) = flatMap(mapper).takeNotNull()
|
||||
|
||||
@JsName("flatMapNotNullIterable")
|
||||
@JvmName("flatMapNotNullIterable")
|
||||
inline fun <T, R> Flow<Iterable<T>>.flatMapNotNull(
|
||||
crossinline mapper: suspend (T) -> R
|
||||
) = flatMap(mapper).takeNotNull()
|
||||
|
||||
fun <T> Flow<Flow<T>>.flatten() = flatMap { it }
|
||||
|
||||
@JsName("flattenIterable")
|
||||
@JvmName("flattenIterable")
|
||||
fun <T> Flow<Iterable<T>>.flatten() = flatMap { it }
|
@@ -0,0 +1,6 @@
|
||||
package dev.inmo.micro_utils.coroutines
|
||||
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
fun <T> Flow<T>.takeNotNull() = mapNotNull { it }
|
||||
fun <T> Flow<T>.filterNotNull() = takeNotNull()
|
@@ -4,6 +4,8 @@ package dev.inmo.micro_utils.coroutines
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
|
||||
/**
|
||||
* Shortcut for chain if [Flow.onEach] and [Flow.launchIn]
|
||||
@@ -29,9 +31,10 @@ inline fun <T> Flow<T>.subscribeSafely(
|
||||
*/
|
||||
inline fun <T> Flow<T>.subscribeSafelyWithoutExceptions(
|
||||
scope: CoroutineScope,
|
||||
noinline onException: ExceptionHandler<T?> = defaultSafelyWithoutExceptionHandlerWithNull,
|
||||
noinline block: suspend (T) -> Unit
|
||||
) = subscribe(scope) {
|
||||
safelyWithoutExceptions {
|
||||
safelyWithoutExceptions(onException) {
|
||||
block(it)
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,118 @@
|
||||
package dev.inmo.micro_utils.coroutines
|
||||
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
|
||||
private class SubscribeAsyncReceiver<T>(
|
||||
val scope: CoroutineScope,
|
||||
output: suspend SubscribeAsyncReceiver<T>.(T) -> Unit
|
||||
) {
|
||||
private val dataChannel: Channel<T> = Channel(Channel.UNLIMITED)
|
||||
val channel: SendChannel<T>
|
||||
get() = dataChannel
|
||||
|
||||
init {
|
||||
scope.launchSafelyWithoutExceptions {
|
||||
for (data in dataChannel) {
|
||||
output(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun isEmpty(): Boolean = dataChannel.isEmpty
|
||||
}
|
||||
|
||||
private sealed interface AsyncSubscriptionCommand<T, M> {
|
||||
suspend operator fun invoke(markersMap: MutableMap<M, SubscribeAsyncReceiver<T>>)
|
||||
}
|
||||
private data class AsyncSubscriptionCommandData<T, M>(
|
||||
val data: T,
|
||||
val scope: CoroutineScope,
|
||||
val markerFactory: suspend (T) -> M,
|
||||
val block: suspend (T) -> Unit,
|
||||
val onEmpty: suspend (M) -> Unit
|
||||
) : AsyncSubscriptionCommand<T, M> {
|
||||
override suspend fun invoke(markersMap: MutableMap<M, SubscribeAsyncReceiver<T>>) {
|
||||
val marker = markerFactory(data)
|
||||
markersMap.getOrPut(marker) {
|
||||
SubscribeAsyncReceiver(scope.LinkedSupervisorScope()) {
|
||||
safelyWithoutExceptions { block(it) }
|
||||
if (isEmpty()) {
|
||||
onEmpty(marker)
|
||||
}
|
||||
}
|
||||
}.channel.send(data)
|
||||
}
|
||||
}
|
||||
|
||||
private data class AsyncSubscriptionCommandClearReceiver<T, M>(
|
||||
val marker: M
|
||||
) : AsyncSubscriptionCommand<T, M> {
|
||||
override suspend fun invoke(markersMap: MutableMap<M, SubscribeAsyncReceiver<T>>) {
|
||||
val receiver = markersMap[marker]
|
||||
if (receiver ?.isEmpty() == true) {
|
||||
markersMap.remove(marker)
|
||||
receiver.scope.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun <T, M> Flow<T>.subscribeAsync(
|
||||
scope: CoroutineScope,
|
||||
markerFactory: suspend (T) -> M,
|
||||
block: suspend (T) -> Unit
|
||||
): Job {
|
||||
val subscope = scope.LinkedSupervisorScope()
|
||||
val markersMap = mutableMapOf<M, SubscribeAsyncReceiver<T>>()
|
||||
val actor = subscope.actor<AsyncSubscriptionCommand<T, M>>(Channel.UNLIMITED) {
|
||||
it.invoke(markersMap)
|
||||
}
|
||||
|
||||
val job = subscribeSafelyWithoutExceptions(subscope) { data ->
|
||||
val dataCommand = AsyncSubscriptionCommandData(data, subscope, markerFactory, block) { marker ->
|
||||
actor.send(
|
||||
AsyncSubscriptionCommandClearReceiver(marker)
|
||||
)
|
||||
}
|
||||
actor.send(dataCommand)
|
||||
}
|
||||
|
||||
job.invokeOnCompletion { if (subscope.isActive) subscope.cancel() }
|
||||
|
||||
return job
|
||||
}
|
||||
|
||||
inline fun <T, M> Flow<T>.subscribeSafelyAsync(
|
||||
scope: CoroutineScope,
|
||||
noinline markerFactory: suspend (T) -> M,
|
||||
noinline onException: ExceptionHandler<Unit> = defaultSafelyExceptionHandler,
|
||||
noinline block: suspend (T) -> Unit
|
||||
) = subscribeAsync(scope, markerFactory) {
|
||||
safely(onException) {
|
||||
block(it)
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <T, M> Flow<T>.subscribeSafelyWithoutExceptionsAsync(
|
||||
scope: CoroutineScope,
|
||||
noinline markerFactory: suspend (T) -> M,
|
||||
noinline onException: ExceptionHandler<T?> = defaultSafelyWithoutExceptionHandlerWithNull,
|
||||
noinline block: suspend (T) -> Unit
|
||||
) = subscribeAsync(scope, markerFactory) {
|
||||
safelyWithoutExceptions(onException) {
|
||||
block(it)
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <T, M> Flow<T>.subscribeSafelySkippingExceptionsAsync(
|
||||
scope: CoroutineScope,
|
||||
noinline markerFactory: suspend (T) -> M,
|
||||
noinline block: suspend (T) -> Unit
|
||||
) = subscribeAsync(scope, markerFactory) {
|
||||
safelyWithoutExceptions({ /* do nothing */}) {
|
||||
block(it)
|
||||
}
|
||||
}
|
@@ -115,10 +115,21 @@ suspend inline fun <T> runCatchingSafely(
|
||||
safely(onException, block)
|
||||
}
|
||||
|
||||
suspend inline fun <T, R> T.runCatchingSafely(
|
||||
noinline onException: ExceptionHandler<R> = defaultSafelyExceptionHandler,
|
||||
noinline block: suspend T.() -> R
|
||||
): Result<R> = runCatching {
|
||||
safely(onException) { block() }
|
||||
}
|
||||
|
||||
suspend inline fun <T> safelyWithResult(
|
||||
noinline block: suspend CoroutineScope.() -> T
|
||||
): Result<T> = runCatchingSafely(defaultSafelyExceptionHandler, block)
|
||||
|
||||
suspend inline fun <T, R> T.safelyWithResult(
|
||||
noinline block: suspend T.() -> R
|
||||
): Result<R> = runCatchingSafely(defaultSafelyExceptionHandler, block)
|
||||
|
||||
/**
|
||||
* Use this handler in cases you wish to include handling of exceptions by [defaultSafelyWithoutExceptionHandler] and
|
||||
* returning null at one time
|
||||
@@ -147,3 +158,10 @@ suspend inline fun <T> runCatchingSafelyWithoutExceptions(
|
||||
): Result<T?> = runCatching {
|
||||
safelyWithoutExceptions(onException, block)
|
||||
}
|
||||
|
||||
inline fun CoroutineScope(
|
||||
context: CoroutineContext,
|
||||
noinline defaultExceptionsHandler: ExceptionHandler<Unit>
|
||||
) = CoroutineScope(
|
||||
context + ContextSafelyExceptionHandler(defaultExceptionsHandler)
|
||||
)
|
||||
|
@@ -0,0 +1,136 @@
|
||||
package dev.inmo.micro_utils.coroutines
|
||||
|
||||
import kotlinx.coroutines.currentCoroutineContext
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlin.contracts.ExperimentalContracts
|
||||
import kotlin.contracts.InvocationKind
|
||||
import kotlin.contracts.contract
|
||||
|
||||
/**
|
||||
* It is interface which will work like classic [Mutex], but in difference have [lockStateFlow] for listening of the
|
||||
* [SmartMutex] state.
|
||||
*
|
||||
* There is [Mutable] and [Immutable] realizations. In case you are owner and manager current state of lock, you need
|
||||
* [Mutable] [SmartMutex]. Otherwise, [Immutable].
|
||||
*
|
||||
* Any [Mutable] [SmartMutex] may produce its [Immutable] variant which will contains [lockStateFlow] equal to its
|
||||
* [Mutable] creator
|
||||
*/
|
||||
sealed interface SmartMutex {
|
||||
val lockStateFlow: StateFlow<Boolean>
|
||||
|
||||
/**
|
||||
* * True - locked
|
||||
* * False - unlocked
|
||||
*/
|
||||
val isLocked: Boolean
|
||||
get() = lockStateFlow.value
|
||||
|
||||
/**
|
||||
* Immutable variant of [SmartMutex]. In fact will depend on the owner of [lockStateFlow]
|
||||
*/
|
||||
class Immutable(override val lockStateFlow: StateFlow<Boolean>) : SmartMutex
|
||||
|
||||
/**
|
||||
* Mutable variant of [SmartMutex]. With that variant you may [lock] and [unlock]. Besides, you may create
|
||||
* [Immutable] variant of [this] instance with [immutable] factory
|
||||
*
|
||||
* @param locked Preset state of [isLocked] and its internal [_lockStateFlow]
|
||||
*/
|
||||
class Mutable(locked: Boolean = false) : SmartMutex {
|
||||
private val _lockStateFlow = MutableStateFlow<Boolean>(locked)
|
||||
override val lockStateFlow: StateFlow<Boolean> = _lockStateFlow.asStateFlow()
|
||||
|
||||
private val internalChangesMutex = Mutex()
|
||||
|
||||
fun immutable() = Immutable(lockStateFlow)
|
||||
|
||||
/**
|
||||
* Holds call until this [SmartMutex] will be re-locked. That means that while [isLocked] == true, [holds] will
|
||||
* wait for [isLocked] == false and then try to lock
|
||||
*/
|
||||
suspend fun lock() {
|
||||
do {
|
||||
waitUnlock()
|
||||
val shouldContinue = internalChangesMutex.withLock {
|
||||
if (_lockStateFlow.value) {
|
||||
true
|
||||
} else {
|
||||
_lockStateFlow.value = true
|
||||
false
|
||||
}
|
||||
}
|
||||
} while (shouldContinue && currentCoroutineContext().isActive)
|
||||
}
|
||||
|
||||
/**
|
||||
* Will try to lock this [SmartMutex] immediataly
|
||||
*
|
||||
* @return True if lock was successful. False otherwise
|
||||
*/
|
||||
suspend fun tryLock(): Boolean {
|
||||
return if (!_lockStateFlow.value) {
|
||||
internalChangesMutex.withLock {
|
||||
if (!_lockStateFlow.value) {
|
||||
_lockStateFlow.value = true
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If [isLocked] == true - will change it to false and return true. If current call will not unlock this
|
||||
* [SmartMutex] - false
|
||||
*/
|
||||
suspend fun unlock(): Boolean {
|
||||
return if (_lockStateFlow.value) {
|
||||
internalChangesMutex.withLock {
|
||||
if (_lockStateFlow.value) {
|
||||
_lockStateFlow.value = false
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Will call [SmartMutex.Mutable.lock], then execute [action] and return the result after [SmartMutex.Mutable.unlock]
|
||||
*/
|
||||
@OptIn(ExperimentalContracts::class)
|
||||
suspend inline fun <T> SmartMutex.Mutable.withLock(action: () -> T): T {
|
||||
contract {
|
||||
callsInPlace(action, InvocationKind.EXACTLY_ONCE)
|
||||
}
|
||||
|
||||
lock()
|
||||
try {
|
||||
return action()
|
||||
} finally {
|
||||
unlock()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Will wait until the [SmartMutex.lockStateFlow] of [this] instance will be false.
|
||||
*
|
||||
* Anyway, after the end of this block there are no any guaranties that [SmartMutex.isLocked] == false due to the fact
|
||||
* that some other parties may lock it again
|
||||
*/
|
||||
suspend fun SmartMutex.waitUnlock() = lockStateFlow.first { !it }
|
@@ -0,0 +1,28 @@
|
||||
package dev.inmo.micro_utils.coroutines
|
||||
|
||||
import dev.inmo.micro_utils.common.onRemoved
|
||||
import dev.inmo.micro_utils.common.onVisibilityChanged
|
||||
import kotlinx.coroutines.flow.*
|
||||
import org.w3c.dom.Element
|
||||
|
||||
fun Element.visibilityFlow(): Flow<Boolean> = channelFlow {
|
||||
var previousData: Boolean? = null
|
||||
|
||||
val observer = onVisibilityChanged { intersectionRatio, _ ->
|
||||
val currentData = intersectionRatio > 0
|
||||
if (currentData != previousData) {
|
||||
trySend(currentData)
|
||||
}
|
||||
previousData = currentData
|
||||
}
|
||||
|
||||
val removeObserver = onRemoved {
|
||||
observer.disconnect()
|
||||
close()
|
||||
}
|
||||
|
||||
invokeOnClose {
|
||||
observer.disconnect()
|
||||
removeObserver.disconnect()
|
||||
}
|
||||
}
|
@@ -0,0 +1,26 @@
|
||||
package dev.inmo.micro_utils.coroutines
|
||||
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Job
|
||||
import org.w3c.dom.Image
|
||||
|
||||
suspend fun preloadImage(src: String): Image {
|
||||
val image = Image()
|
||||
image.src = src
|
||||
|
||||
val job = Job()
|
||||
|
||||
image.addEventListener("load", {
|
||||
runCatching { job.complete() }
|
||||
})
|
||||
|
||||
runCatchingSafely {
|
||||
job.join()
|
||||
}.onFailure {
|
||||
if (it is CancellationException) {
|
||||
image.src = ""
|
||||
}
|
||||
}.getOrThrow()
|
||||
|
||||
return image
|
||||
}
|
@@ -0,0 +1,42 @@
|
||||
package dev.inmo.micro_utils.coroutines
|
||||
|
||||
import dev.inmo.micro_utils.common.MPPFile
|
||||
import dev.inmo.micro_utils.common.selectFile
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import org.w3c.dom.HTMLInputElement
|
||||
|
||||
suspend fun selectFileOrThrow(
|
||||
inputSetup: (HTMLInputElement) -> Unit = {}
|
||||
): MPPFile {
|
||||
val result = CompletableDeferred<MPPFile>()
|
||||
|
||||
selectFile(
|
||||
inputSetup,
|
||||
{
|
||||
result.completeExceptionally(it)
|
||||
}
|
||||
) {
|
||||
result.complete(it)
|
||||
}
|
||||
|
||||
return result.await()
|
||||
}
|
||||
|
||||
suspend fun selectFileOrNull(
|
||||
inputSetup: (HTMLInputElement) -> Unit = {},
|
||||
onFailure: (Throwable) -> Unit = {}
|
||||
): MPPFile? {
|
||||
val result = CompletableDeferred<MPPFile?>()
|
||||
|
||||
selectFile(
|
||||
inputSetup,
|
||||
{
|
||||
result.complete(null)
|
||||
onFailure(it)
|
||||
}
|
||||
) {
|
||||
result.complete(it)
|
||||
}
|
||||
|
||||
return result.await()
|
||||
}
|
@@ -0,0 +1,9 @@
|
||||
package dev.inmo.micro_utils.coroutines
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
||||
fun <T> launchInCurrentThread(block: suspend CoroutineScope.() -> T): T {
|
||||
val scope = CoroutineScope(Dispatchers.Unconfined)
|
||||
return scope.launchSynchronously(block)
|
||||
}
|
@@ -6,7 +6,7 @@ fun <T> CoroutineScope.launchSynchronously(block: suspend CoroutineScope.() -> T
|
||||
var result: Result<T>? = null
|
||||
val objectToSynchronize = Object()
|
||||
synchronized(objectToSynchronize) {
|
||||
launch {
|
||||
launch(start = CoroutineStart.UNDISPATCHED) {
|
||||
result = safelyWithResult(block)
|
||||
}.invokeOnCompletion {
|
||||
synchronized(objectToSynchronize) {
|
||||
|
@@ -0,0 +1,47 @@
|
||||
package dev.inmo.micro_utils.coroutines
|
||||
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class LaunchInCurrentThreadTests {
|
||||
@Test
|
||||
fun simpleTestThatLaunchInCurrentThreadWorks() {
|
||||
val expectedResult = 10
|
||||
val result = launchInCurrentThread {
|
||||
expectedResult
|
||||
}
|
||||
assertEquals(expectedResult, result)
|
||||
}
|
||||
@Test
|
||||
fun simpleTestThatSeveralLaunchInCurrentThreadWorks() {
|
||||
val testData = 0 until 100
|
||||
|
||||
testData.forEach {
|
||||
val result = launchInCurrentThread {
|
||||
it
|
||||
}
|
||||
assertEquals(it, result)
|
||||
}
|
||||
}
|
||||
@Test
|
||||
fun simpleTestThatLaunchInCurrentThreadWillCorrectlyHandleSuspensionsWorks() {
|
||||
val testData = 0 until 100
|
||||
|
||||
suspend fun test(data: Any): Any {
|
||||
return withContext(Dispatchers.Default) {
|
||||
delay(1)
|
||||
data
|
||||
}
|
||||
}
|
||||
|
||||
testData.forEach {
|
||||
val result = launchInCurrentThread {
|
||||
test(it)
|
||||
}
|
||||
assertEquals(it, result)
|
||||
}
|
||||
}
|
||||
}
|
50
coroutines/src/main/kotlin/FlowOnHierarchyChangeListener.kt
Normal file
50
coroutines/src/main/kotlin/FlowOnHierarchyChangeListener.kt
Normal file
@@ -0,0 +1,50 @@
|
||||
package dev.inmo.micro_utils.coroutines
|
||||
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
|
||||
/**
|
||||
* [kotlinx.coroutines.flow.Flow]-based [android.view.ViewGroup.OnHierarchyChangeListener]
|
||||
*
|
||||
* @param recursive If set, any call of [onChildViewAdded] will check if child [View] is [ViewGroup] and subscribe to this
|
||||
* [ViewGroup] too
|
||||
* @param [_onChildViewAdded] Internal [MutableSharedFlow] which will be used to pass data to [onChildViewAdded] flow
|
||||
* @param [_onChildViewRemoved] Internal [MutableSharedFlow] which will be used to pass data to [onChildViewRemoved] flow
|
||||
*/
|
||||
class FlowOnHierarchyChangeListener(
|
||||
private val recursive: Boolean = false,
|
||||
private val _onChildViewAdded: MutableSharedFlow<Pair<View, View>> = MutableSharedFlow(extraBufferCapacity = Int.MAX_VALUE),
|
||||
private val _onChildViewRemoved: MutableSharedFlow<Pair<View, View>> = MutableSharedFlow(extraBufferCapacity = Int.MAX_VALUE)
|
||||
) : ViewGroup.OnHierarchyChangeListener {
|
||||
val onChildViewAdded = _onChildViewAdded.asSharedFlow()
|
||||
val onChildViewRemoved = _onChildViewRemoved.asSharedFlow()
|
||||
|
||||
/**
|
||||
* Will emit data into [onChildViewAdded] flow. If [recursive] is true and [child] is [ViewGroup] will also
|
||||
* subscribe to [child] hierarchy changes.
|
||||
*
|
||||
* Due to the fact that this method is not suspendable, [FlowOnHierarchyChangeListener] will use
|
||||
* [MutableSharedFlow.tryEmit] to send data into [_onChildViewAdded]. That is why its default extraBufferCapacity is
|
||||
* [Int.MAX_VALUE]
|
||||
*/
|
||||
override fun onChildViewAdded(parent: View, child: View) {
|
||||
_onChildViewAdded.tryEmit(parent to child)
|
||||
|
||||
if (recursive && child is ViewGroup) {
|
||||
child.setOnHierarchyChangeListener(this)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Just emit data into [onChildViewRemoved]
|
||||
*
|
||||
* Due to the fact that this method is not suspendable, [FlowOnHierarchyChangeListener] will use
|
||||
* [MutableSharedFlow.tryEmit] to send data into [_onChildViewRemoved]. That is why its default extraBufferCapacity is
|
||||
* [Int.MAX_VALUE]
|
||||
*/
|
||||
override fun onChildViewRemoved(parent: View, child: View) {
|
||||
_onChildViewRemoved.tryEmit(parent to child)
|
||||
}
|
||||
}
|
17
coroutines/src/main/kotlin/RecursiveHierarchySubscriber.kt
Normal file
17
coroutines/src/main/kotlin/RecursiveHierarchySubscriber.kt
Normal file
@@ -0,0 +1,17 @@
|
||||
package dev.inmo.micro_utils.coroutines
|
||||
|
||||
import android.view.ViewGroup
|
||||
import android.view.ViewGroup.OnHierarchyChangeListener
|
||||
|
||||
/**
|
||||
* Use [ViewGroup.setOnHierarchyChangeListener] recursively for all available [ViewGroup]s starting with [this].
|
||||
* This extension DO NOT guarantee that recursive subscription will happen after this method call
|
||||
*/
|
||||
fun ViewGroup.setOnHierarchyChangeListenerRecursively(
|
||||
listener: OnHierarchyChangeListener
|
||||
) {
|
||||
setOnHierarchyChangeListener(listener)
|
||||
(0 until childCount).forEach {
|
||||
(getChildAt(it) as? ViewGroup) ?.setOnHierarchyChangeListenerRecursively(listener)
|
||||
}
|
||||
}
|
@@ -11,6 +11,7 @@ kotlin {
|
||||
commonMain {
|
||||
dependencies {
|
||||
api project(":micro_utils.common")
|
||||
api libs.krypto
|
||||
}
|
||||
}
|
||||
jsMain {
|
||||
|
@@ -0,0 +1,15 @@
|
||||
package dev.inmo.micro_utils.crypto
|
||||
|
||||
val HEX_ARRAY = "0123456789abcdef".toCharArray()
|
||||
|
||||
fun SourceBytes.hex(): String {
|
||||
val hexChars = CharArray(size * 2)
|
||||
for (j in indices) {
|
||||
val v: Int = this[j].toInt() and 0xFF
|
||||
hexChars[j * 2] = HEX_ARRAY[v ushr 4]
|
||||
hexChars[j * 2 + 1] = HEX_ARRAY[v and 0x0F]
|
||||
}
|
||||
return hexChars.concatToString()
|
||||
}
|
||||
|
||||
fun SourceString.hex(): String = encodeToByteArray().hex()
|
@@ -1,6 +1,8 @@
|
||||
package dev.inmo.micro_utils.crypto
|
||||
|
||||
import korlibs.crypto.md5
|
||||
|
||||
typealias MD5 = String
|
||||
|
||||
expect fun SourceBytes.md5(): MD5
|
||||
fun SourceString.md5(): MD5 = encodeToByteArray().md5()
|
||||
fun SourceBytes.md5(): MD5 = md5().hexLower
|
||||
fun SourceString.md5(): MD5 = encodeToByteArray().md5().hexLower
|
||||
|
@@ -0,0 +1,15 @@
|
||||
package dev.inmo.micro_utils.crypto
|
||||
|
||||
import kotlin.test.*
|
||||
|
||||
class Hex {
|
||||
@Test
|
||||
fun testSimpleHmacSHA256Message() {
|
||||
val text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
|
||||
val resultHex = text.hex()
|
||||
assertEquals(
|
||||
"4c6f72656d20697073756d20646f6c6f722073697420616d65742c20636f6e73656374657475722061646970697363696e6720656c69742c2073656420646f20656975736d6f642074656d706f7220696e6369646964756e74207574206c61626f726520657420646f6c6f7265206d61676e6120616c697175612e20557420656e696d206164206d696e696d2076656e69616d2c2071756973206e6f737472756420657865726369746174696f6e20756c6c616d636f206c61626f726973206e69736920757420616c697175697020657820656120636f6d6d6f646f20636f6e7365717561742e2044756973206175746520697275726520646f6c6f7220696e20726570726568656e646572697420696e20766f6c7570746174652076656c697420657373652063696c6c756d20646f6c6f726520657520667567696174206e756c6c612070617269617475722e204578636570746575722073696e74206f6363616563617420637570696461746174206e6f6e2070726f6964656e742c2073756e7420696e2063756c706120717569206f666669636961206465736572756e74206d6f6c6c697420616e696d20696420657374206c61626f72756d2e",
|
||||
resultHex
|
||||
)
|
||||
}
|
||||
}
|
@@ -1,3 +0,0 @@
|
||||
package dev.inmo.micro_utils.crypto
|
||||
|
||||
actual fun SourceBytes.md5(): MD5 = CryptoJS.MD5(decodeToString())
|
@@ -1,9 +0,0 @@
|
||||
package dev.inmo.micro_utils.crypto
|
||||
|
||||
import java.math.BigInteger
|
||||
import java.security.MessageDigest
|
||||
|
||||
actual fun SourceBytes.md5(): MD5 = BigInteger(
|
||||
1,
|
||||
MessageDigest.getInstance("MD5").digest(this)
|
||||
).toString(16)
|
@@ -1,37 +1,12 @@
|
||||
apply plugin: 'com.getkeepsafe.dexcount'
|
||||
|
||||
ext {
|
||||
jvmKotlinFolderFile = {
|
||||
String sep = File.separator
|
||||
return new File("${project.projectDir}${sep}src${sep}jvmMain${sep}kotlin")
|
||||
}
|
||||
|
||||
enableIncludingJvmCodeInAndroidPart = {
|
||||
File jvmKotlinFolder = jvmKotlinFolderFile()
|
||||
if (jvmKotlinFolder.exists()) {
|
||||
android.sourceSets.main.java.srcDirs += jvmKotlinFolder.path
|
||||
}
|
||||
}
|
||||
|
||||
disableIncludingJvmCodeInAndroidPart = {
|
||||
File jvmKotlinFolder = jvmKotlinFolderFile()
|
||||
String[] oldDirs = android.sourceSets.main.java.srcDirs
|
||||
android.sourceSets.main.java.srcDirs = []
|
||||
for (oldDir in oldDirs) {
|
||||
if (oldDir != jvmKotlinFolder.path) {
|
||||
android.sourceSets.main.java.srcDirs += oldDir
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion "$android_compileSdkVersion".toInteger()
|
||||
buildToolsVersion "$android_buildToolsVersion"
|
||||
compileSdkVersion libs.versions.android.props.compileSdk.get().toInteger()
|
||||
buildToolsVersion libs.versions.android.props.buildTools.get()
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion "$android_minSdkVersion".toInteger()
|
||||
targetSdkVersion "$android_compileSdkVersion".toInteger()
|
||||
minSdkVersion libs.versions.android.props.minSdk.get().toInteger()
|
||||
targetSdkVersion libs.versions.android.props.compileSdk.get().toInteger()
|
||||
versionCode "${android_code_version}".toInteger()
|
||||
versionName "$version"
|
||||
}
|
||||
@@ -58,10 +33,4 @@ android {
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_1_8.toString()
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
String sep = File.separator
|
||||
main.java.srcDirs += "src${sep}main${sep}kotlin"
|
||||
enableIncludingJvmCodeInAndroidPart()
|
||||
}
|
||||
}
|
||||
|
@@ -7,14 +7,13 @@ plugins {
|
||||
|
||||
repositories {
|
||||
mavenLocal()
|
||||
jcenter()
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvm()
|
||||
js(BOTH) {
|
||||
js(IR) {
|
||||
browser()
|
||||
nodejs()
|
||||
}
|
||||
@@ -118,15 +117,15 @@ tasks.dokkaHtml {
|
||||
}
|
||||
|
||||
named("jsMain") {
|
||||
sourceRoots.setFrom(findSourcesWithName("jsMain", "commonMain"))
|
||||
sourceRoots.setFrom(findSourcesWithName("jsMain"))
|
||||
}
|
||||
|
||||
named("jvmMain") {
|
||||
sourceRoots.setFrom(findSourcesWithName("jvmMain", "commonMain"))
|
||||
sourceRoots.setFrom(findSourcesWithName("jvmMain"))
|
||||
}
|
||||
|
||||
named("androidMain") {
|
||||
sourceRoots.setFrom(findSourcesWithName("androidMain", "commonMain"))
|
||||
sourceRoots.setFrom(findSourcesWithName("androidMain"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -21,12 +21,14 @@ allprojects {
|
||||
releaseMode = (project.hasProperty('RELEASE_MODE') && project.property('RELEASE_MODE') == "true") || System.getenv('RELEASE_MODE') == "true"
|
||||
|
||||
mppProjectWithSerializationPresetPath = "${rootProject.projectDir.absolutePath}/mppProjectWithSerialization.gradle"
|
||||
mppProjectWithSerializationAndComposePresetPath = "${rootProject.projectDir.absolutePath}/mppProjectWithSerializationAndCompose.gradle"
|
||||
mppJavaProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppJavaProject.gradle"
|
||||
mppJvmJsLinuxMingwProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppJvmJsLinuxMingwProject.gradle"
|
||||
mppAndroidProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppAndroidProject.gradle"
|
||||
|
||||
defaultAndroidSettingsPresetPath = "${rootProject.projectDir.absolutePath}/defaultAndroidSettings.gradle"
|
||||
|
||||
publishGradlePath = "${rootProject.projectDir.absolutePath}/publish.gradle"
|
||||
publishMavenPath = "${rootProject.projectDir.absolutePath}/maven.publish.gradle"
|
||||
publishJvmOnlyPath = "${rootProject.projectDir.absolutePath}/jvm.publish.gradle"
|
||||
}
|
||||
}
|
||||
|
18
fsm/common/build.gradle
Normal file
18
fsm/common/build.gradle
Normal file
@@ -0,0 +1,18 @@
|
||||
plugins {
|
||||
id "org.jetbrains.kotlin.multiplatform"
|
||||
id "org.jetbrains.kotlin.plugin.serialization"
|
||||
id "com.android.library"
|
||||
}
|
||||
|
||||
apply from: "$mppProjectWithSerializationPresetPath"
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
commonMain {
|
||||
dependencies {
|
||||
api project(":micro_utils.common")
|
||||
api project(":micro_utils.coroutines")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,68 @@
|
||||
package dev.inmo.micro_utils.fsm.common
|
||||
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
/**
|
||||
* Define checkable holder which can be used to precheck that this handler may handle incoming [State]
|
||||
*/
|
||||
interface CheckableHandlerHolder<I : State, O : State> : StatesHandler<I, O> {
|
||||
suspend fun checkHandleable(state: O): Boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Default realization of [StatesHandler]. It will incapsulate checking of [State] type in [checkHandleable] and class
|
||||
* casting in [handleState]
|
||||
*/
|
||||
class CustomizableHandlerHolder<I : O, O : State>(
|
||||
private val delegateTo: StatesHandler<I, O>,
|
||||
private val filter: suspend (state: O) -> Boolean
|
||||
) : CheckableHandlerHolder<I, O> {
|
||||
/**
|
||||
* Checks that [state] can be handled by [delegateTo]. Under the hood it will check exact equality of [state]
|
||||
* [KClass] and use [KClass.isInstance] of [inputKlass] if [strict] == false
|
||||
*/
|
||||
override suspend fun checkHandleable(state: O) = filter(state)
|
||||
|
||||
/**
|
||||
* Calls [delegateTo] method [StatesHandler.handleState] with [state] casted to [I]. Use [checkHandleable]
|
||||
* to be sure that this [StatesHandlerHolder] will be able to handle [state]
|
||||
*/
|
||||
override suspend fun StatesMachine<in O>.handleState(state: I): O? {
|
||||
return delegateTo.run { handleState(state) }
|
||||
}
|
||||
}
|
||||
|
||||
fun <I : O, O : State> CheckableHandlerHolder(
|
||||
inputKlass: KClass<I>,
|
||||
strict: Boolean = false,
|
||||
delegateTo: StatesHandler<I, O>
|
||||
) = CustomizableHandlerHolder(
|
||||
StatesHandler<O, O> {
|
||||
delegateTo.run { handleState(it as I) }
|
||||
},
|
||||
if (strict) {
|
||||
{ it::class == inputKlass }
|
||||
} else {
|
||||
{ inputKlass.isInstance(it) }
|
||||
}
|
||||
)
|
||||
|
||||
inline fun <reified I : O, O : State> CheckableHandlerHolder(
|
||||
strict: Boolean = false,
|
||||
delegateTo: StatesHandler<I, O>
|
||||
) = CheckableHandlerHolder(I::class, strict, delegateTo)
|
||||
|
||||
inline fun <reified I : O, O: State> StatesHandler<I, O>.holder(
|
||||
strict: Boolean = true
|
||||
) = CheckableHandlerHolder<I, O>(
|
||||
I::class,
|
||||
strict,
|
||||
this
|
||||
)
|
||||
|
||||
inline fun <I : O, O: State> StatesHandler<I, O>.holder(
|
||||
noinline filter: suspend (state: State) -> Boolean
|
||||
) = CustomizableHandlerHolder<O, O>(
|
||||
{ this@holder.run { handleState(it as I) } },
|
||||
filter
|
||||
)
|
@@ -0,0 +1,5 @@
|
||||
package dev.inmo.micro_utils.fsm.common
|
||||
|
||||
interface State {
|
||||
val context: Any
|
||||
}
|
@@ -0,0 +1,12 @@
|
||||
package dev.inmo.micro_utils.fsm.common
|
||||
|
||||
/**
|
||||
* Default realization of states handler
|
||||
*/
|
||||
fun interface StatesHandler<I : State, O: State> {
|
||||
/**
|
||||
* Main handling of [state]. In case when this [state] leads to another [State] and [handleState] returns not null
|
||||
* [State] it is assumed that chain is not completed.
|
||||
*/
|
||||
suspend fun StatesMachine<in O>.handleState(state: I): O?
|
||||
}
|
@@ -0,0 +1,146 @@
|
||||
package dev.inmo.micro_utils.fsm.common
|
||||
|
||||
import dev.inmo.micro_utils.common.Optional
|
||||
import dev.inmo.micro_utils.common.onPresented
|
||||
import dev.inmo.micro_utils.coroutines.*
|
||||
import dev.inmo.micro_utils.fsm.common.utils.StateHandlingErrorHandler
|
||||
import dev.inmo.micro_utils.fsm.common.utils.defaultStateHandlingErrorHandler
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.asFlow
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
|
||||
/**
|
||||
* Default [StatesMachine] may [startChain] and use inside logic for handling [State]s. By default you may use
|
||||
* [DefaultStatesMachine] or build it with [dev.inmo.micro_utils.fsm.common.dsl.buildFSM]. Implementers MUST NOT start
|
||||
* handling until [start] method will be called
|
||||
*/
|
||||
interface StatesMachine<T : State> : StatesHandler<T, T> {
|
||||
suspend fun launchStateHandling(
|
||||
state: T,
|
||||
handlers: List<CheckableHandlerHolder<in T, T>>,
|
||||
onStateHandlingErrorHandler: StateHandlingErrorHandler<T>
|
||||
): T? {
|
||||
return runCatchingSafely {
|
||||
handlers.firstOrNull { it.checkHandleable(state) } ?.run {
|
||||
handleState(state)
|
||||
}
|
||||
}.getOrElse {
|
||||
onStateHandlingErrorHandler(state, it)
|
||||
}
|
||||
}
|
||||
suspend fun launchStateHandling(
|
||||
state: T,
|
||||
handlers: List<CheckableHandlerHolder<in T, T>>
|
||||
): T? {
|
||||
return launchStateHandling(state, handlers, defaultStateHandlingErrorHandler())
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts handling of [State]s
|
||||
*/
|
||||
fun start(scope: CoroutineScope): Job
|
||||
|
||||
/**
|
||||
* Start chain of [State]s witn [state]
|
||||
*/
|
||||
suspend fun startChain(state: T)
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Creates [DefaultStatesMachine]
|
||||
*/
|
||||
operator fun <T: State> invoke(
|
||||
statesManager: StatesManager<T>,
|
||||
handlers: List<CheckableHandlerHolder<in T, T>>,
|
||||
onStateHandlingErrorHandler: StateHandlingErrorHandler<T> = defaultStateHandlingErrorHandler()
|
||||
) = DefaultStatesMachine(statesManager, handlers, onStateHandlingErrorHandler)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Default realization of [StatesMachine]. It uses [statesManager] for incapsulation of [State]s storing and contexts
|
||||
* resolving, and uses [launchStateHandling] for [State] handling.
|
||||
*
|
||||
* This class suppose to be extended in case you wish some custom behaviour inside of [launchStateHandling], for example
|
||||
*/
|
||||
open class DefaultStatesMachine <T: State>(
|
||||
protected val statesManager: StatesManager<T>,
|
||||
protected val handlers: List<CheckableHandlerHolder<in T, T>>,
|
||||
protected val onStateHandlingErrorHandler: StateHandlingErrorHandler<T> = defaultStateHandlingErrorHandler()
|
||||
) : StatesMachine<T> {
|
||||
/**
|
||||
* Will call [launchStateHandling] for state handling
|
||||
*/
|
||||
override suspend fun StatesMachine<in T>.handleState(state: T): T? = launchStateHandling(state, handlers)
|
||||
|
||||
override suspend fun launchStateHandling(state: T, handlers: List<CheckableHandlerHolder<in T, T>>): T? {
|
||||
return launchStateHandling(state, handlers, onStateHandlingErrorHandler)
|
||||
}
|
||||
|
||||
/**
|
||||
* This
|
||||
*/
|
||||
protected val statesJobs = mutableMapOf<T, Job>()
|
||||
protected val statesJobsMutex = Mutex()
|
||||
|
||||
protected open suspend fun performUpdate(state: T) {
|
||||
val newState = launchStateHandling(state, handlers)
|
||||
if (newState == null) {
|
||||
statesManager.endChain(state)
|
||||
} else {
|
||||
statesManager.update(state, newState)
|
||||
}
|
||||
}
|
||||
|
||||
open suspend fun performStateUpdate(previousState: Optional<T>, actualState: T, scope: CoroutineScope) {
|
||||
statesJobsMutex.withLock {
|
||||
statesJobs[actualState] ?.cancel()
|
||||
statesJobs[actualState] = scope.launch {
|
||||
performUpdate(actualState)
|
||||
}.also { job ->
|
||||
job.invokeOnCompletion { _ ->
|
||||
scope.launch {
|
||||
statesJobsMutex.withLock {
|
||||
if (statesJobs[actualState] == job) {
|
||||
statesJobs.remove(actualState)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Launch handling of states. On [statesManager] [StatesManager.onStartChain],
|
||||
* [statesManager] [StatesManager.onChainStateUpdated] will be called lambda with performing of state. If
|
||||
* [launchStateHandling] will returns some [State] then [statesManager] [StatesManager.update] will be used, otherwise
|
||||
* [StatesManager.endChain].
|
||||
*/
|
||||
override fun start(scope: CoroutineScope): Job = scope.launchSafelyWithoutExceptions {
|
||||
(statesManager.getActiveStates().asFlow() + statesManager.onStartChain).subscribeSafelyWithoutExceptions(this) {
|
||||
launch { performStateUpdate(Optional.absent(), it, scope.LinkedSupervisorScope()) }
|
||||
}
|
||||
statesManager.onChainStateUpdated.subscribeSafelyWithoutExceptions(this) {
|
||||
launch { performStateUpdate(Optional.presented(it.first), it.second, scope.LinkedSupervisorScope()) }
|
||||
}
|
||||
statesManager.onEndChain.subscribeSafelyWithoutExceptions(this) { removedState ->
|
||||
launch {
|
||||
statesJobsMutex.withLock {
|
||||
val stateInMap = statesJobs.keys.firstOrNull { stateInMap -> stateInMap == removedState }
|
||||
if (stateInMap === removedState) {
|
||||
statesJobs[stateInMap] ?.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Just calls [StatesManager.startChain] of [statesManager]
|
||||
*/
|
||||
override suspend fun startChain(state: T) {
|
||||
statesManager.startChain(state)
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user