mirror of
https://github.com/InsanusMokrassar/MicroUtils.git
synced 2025-09-17 14:29:24 +00:00
Compare commits
967 Commits
Author | SHA1 | Date | |
---|---|---|---|
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 | |||
96e97d1691 | |||
261d8827e3 | |||
c3156f2e41 | |||
8c08801460 | |||
aaf1299da7 | |||
a411355b4f | |||
eba41066b4 | |||
f295dff8a2 | |||
a16815143c | |||
6ff3f6ae42 | |||
84071881af | |||
7cccf7e56e | |||
2516d5e381 | |||
cdec8bac75 | |||
fa30aae194 | |||
eb959a3135 | |||
24033e0cac | |||
71f9a505e0 | |||
979b8f017b | |||
af78f01682 | |||
0b16d5c826 | |||
597e14bc7e | |||
04a95867e2 | |||
e0d5eb45b7 | |||
b90cab318e | |||
3252b61abe | |||
2a2da21ff3 | |||
04ef371337 | |||
623e0cd369 | |||
1f466747f0 | |||
2215462f99 | |||
ac4c0a2e4c | |||
f7496db5ac | |||
3028fe975d | |||
23a5034493 | |||
65e339f811 | |||
2020e48659 | |||
9566d6f81f | |||
a00d734712 | |||
27a3e8706a | |||
e601efcfc0 | |||
2bfad9f885 | |||
e78e984943 | |||
242f4b02d0 | |||
041be5a1d1 | |||
976ce056c1 | |||
00c23c73a8 | |||
9dd1848337 | |||
9b30efd9a2 | |||
5853f7cc49 | |||
7b00a06f3e | |||
9ef9be0f37 | |||
13ca419473 | |||
b80f1a0773 | |||
e85101c74e | |||
90668bdf63 | |||
e1a00079a5 | |||
e3add4df42 | |||
ced1a3bccb | |||
8d13a14343 | |||
7238e1ea8a | |||
bd0423f243 | |||
b0441c134c | |||
5860901c30 | |||
df4eaea4b9 | |||
e9223d5502 | |||
702c5a3e5d | |||
ccbed95cdc | |||
c4e2c06cf5 | |||
a6135738a3 | |||
ad1ea985b8 | |||
2058950d07 | |||
4dc27f4489 | |||
122daa3220 | |||
e30928e23d | |||
30292306bb | |||
2f9aa585f1 | |||
9e02c3e5ff | |||
7f813a519b | |||
b5072486b4 | |||
50c1cd8215 | |||
ed4812e6d8 | |||
59e0e751f1 | |||
a5ae5e6c2b | |||
a2b87e63c9 | |||
d95c283653 | |||
6a9ae0c148 | |||
040dd517d8 | |||
9663c1ca64 | |||
ffc2d23be7 | |||
49b009e59b | |||
778b6a555b | |||
b3730998e9 | |||
837758aebe | |||
6ac4149aa1 | |||
278584ae6a | |||
126f9d5f41 | |||
ce15ff4e0a | |||
382b956beb | |||
36deab4909 | |||
550fc59d9d | |||
9100a57458 | |||
74d9bbccd9 | |||
75e602a349 | |||
e73644db10 | |||
148e6bdae7 | |||
1b540199f0 | |||
30b70e9984 | |||
c1557cff27 | |||
2d662f91b3 | |||
c4a08e52e5 | |||
08c371c142 | |||
8e62dd460c | |||
1f9302dc94 | |||
16f445f699 | |||
b4abd564ec | |||
14ffafb0a7 | |||
e0cc780887 | |||
ab7d277167 | |||
8308c1df4d | |||
12c29f5180 | |||
2963098870 | |||
9f56b0a26d | |||
75851312fd | |||
3a3be138a5 | |||
fb7d1f18b0 | |||
d1c6c7696a | |||
5f38d9635d | |||
8185ea87b1 | |||
98bd07d025 | |||
c502c70a21 | |||
d933dc532b | |||
42594f0656 | |||
4b83ca19c3 | |||
bab13f5e83 | |||
14c5f5a26c | |||
b26a4f24d4 | |||
498ec673dc | |||
5c67ab6aec | |||
f78359b5d7 | |||
dbce612cb2 | |||
4322ffdb0a | |||
123b848d74 | |||
b120fbd2b1 | |||
d41c3c2de4 | |||
a3cabd7a9e | |||
3dde486126 | |||
5978122dda | |||
4b4806ce34 | |||
987c6cc709 | |||
012c7e9bdf | |||
d06bb265e5 | |||
e2fb8bf21e | |||
60dea2a518 | |||
3877f49278 | |||
a299a5b505 | |||
a03f7201d2 | |||
94e26ee8a0 | |||
2d8ad47083 | |||
e9aec238a1 | |||
844c33166c | |||
cfe9f2159f | |||
61bccd5f48 | |||
e5a608a315 | |||
9b4f35eea1 |
24
.github/workflows/build.yml
vendored
Normal file
24
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
|
||||||
|
name: Build
|
||||||
|
on: [push]
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: actions/setup-java@v1
|
||||||
|
with:
|
||||||
|
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 }}
|
21
.github/workflows/dokka_push.yml
vendored
Normal file
21
.github/workflows/dokka_push.yml
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
name: Publish KDocs
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
jobs:
|
||||||
|
publishing:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: actions/setup-java@v1
|
||||||
|
with:
|
||||||
|
java-version: 11
|
||||||
|
- name: Build
|
||||||
|
run: ./gradlew build && ./gradlew dokkaHtml
|
||||||
|
- name: Publish KDocs
|
||||||
|
uses: peaceiris/actions-gh-pages@v3
|
||||||
|
with:
|
||||||
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
publish_dir: ./dokka/build/dokka/html
|
||||||
|
publish_branch: kdocs
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -11,5 +11,6 @@ out/
|
|||||||
|
|
||||||
secret.gradle
|
secret.gradle
|
||||||
local.properties
|
local.properties
|
||||||
|
kotlin-js-store
|
||||||
|
|
||||||
publishing.sh
|
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
|
|
1381
CHANGELOG.md
1381
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
@@ -10,7 +10,7 @@ kotlin {
|
|||||||
sourceSets {
|
sourceSets {
|
||||||
androidMain {
|
androidMain {
|
||||||
dependencies {
|
dependencies {
|
||||||
api "androidx.appcompat:appcompat-resources:$appcompat_version"
|
api libs.android.appCompat.resources
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -15,7 +15,7 @@ data class AlertAction(
|
|||||||
val callback: (DialogInterface) -> Unit
|
val callback: (DialogInterface) -> Unit
|
||||||
)
|
)
|
||||||
|
|
||||||
private class ActionViewHolder(
|
class ActionViewHolder(
|
||||||
container: ViewGroup, dialogInterfaceGetter: () -> DialogInterface
|
container: ViewGroup, dialogInterfaceGetter: () -> DialogInterface
|
||||||
) : AbstractStandardViewHolder<AlertAction>(container, android.R.layout.simple_list_item_1) {
|
) : AbstractStandardViewHolder<AlertAction>(container, android.R.layout.simple_list_item_1) {
|
||||||
private lateinit var action: AlertAction
|
private lateinit var action: AlertAction
|
||||||
@@ -34,10 +34,10 @@ private class ActionViewHolder(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class ActionsRecyclerViewAdapter(
|
class ActionsRecyclerViewAdapter(
|
||||||
data: List<AlertAction>,
|
override val data: List<AlertAction>,
|
||||||
private val dialogInterfaceGetter: () -> DialogInterface
|
private val dialogInterfaceGetter: () -> DialogInterface
|
||||||
) : RecyclerViewAdapter<AlertAction>(data) {
|
) : RecyclerViewAdapter<AlertAction>() {
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AbstractViewHolder<AlertAction> = ActionViewHolder(
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AbstractViewHolder<AlertAction> = ActionViewHolder(
|
||||||
parent, dialogInterfaceGetter
|
parent, dialogInterfaceGetter
|
||||||
)
|
)
|
||||||
|
@@ -10,12 +10,13 @@ kotlin {
|
|||||||
sourceSets {
|
sourceSets {
|
||||||
commonMain {
|
commonMain {
|
||||||
dependencies {
|
dependencies {
|
||||||
api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version"
|
api libs.kt.coroutines
|
||||||
|
api project(":micro_utils.common")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
androidMain {
|
androidMain {
|
||||||
dependencies {
|
dependencies {
|
||||||
api "androidx.recyclerview:recyclerview:$androidx_recycler_version"
|
api libs.android.recyclerView
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,12 +1,21 @@
|
|||||||
package dev.inmo.micro_utils.android.recyclerview
|
package dev.inmo.micro_utils.android.recyclerview
|
||||||
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import kotlinx.coroutines.flow.*
|
||||||
|
|
||||||
|
|
||||||
abstract class RecyclerViewAdapter<T>(
|
abstract class RecyclerViewAdapter<T>: RecyclerView.Adapter<AbstractViewHolder<T>>() {
|
||||||
val data: List<T>
|
protected abstract val data: List<T>
|
||||||
): RecyclerView.Adapter<AbstractViewHolder<T>>() {
|
|
||||||
|
private val _dataCountState by lazy {
|
||||||
|
MutableStateFlow<Int>(data.size)
|
||||||
|
}
|
||||||
|
val dataCountState: StateFlow<Int> by lazy {
|
||||||
|
_dataCountState.asStateFlow()
|
||||||
|
}
|
||||||
|
|
||||||
var emptyView: View? = null
|
var emptyView: View? = null
|
||||||
set(value) {
|
set(value) {
|
||||||
field = value
|
field = value
|
||||||
@@ -18,31 +27,37 @@ abstract class RecyclerViewAdapter<T>(
|
|||||||
object : RecyclerView.AdapterDataObserver() {
|
object : RecyclerView.AdapterDataObserver() {
|
||||||
override fun onItemRangeChanged(positionStart: Int, itemCount: Int) {
|
override fun onItemRangeChanged(positionStart: Int, itemCount: Int) {
|
||||||
super.onItemRangeChanged(positionStart, itemCount)
|
super.onItemRangeChanged(positionStart, itemCount)
|
||||||
|
_dataCountState.value = data.size
|
||||||
checkEmpty()
|
checkEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onItemRangeChanged(positionStart: Int, itemCount: Int, payload: Any?) {
|
override fun onItemRangeChanged(positionStart: Int, itemCount: Int, payload: Any?) {
|
||||||
super.onItemRangeChanged(positionStart, itemCount, payload)
|
super.onItemRangeChanged(positionStart, itemCount, payload)
|
||||||
|
_dataCountState.value = data.size
|
||||||
checkEmpty()
|
checkEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onChanged() {
|
override fun onChanged() {
|
||||||
super.onChanged()
|
super.onChanged()
|
||||||
|
_dataCountState.value = data.size
|
||||||
checkEmpty()
|
checkEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) {
|
override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) {
|
||||||
super.onItemRangeRemoved(positionStart, itemCount)
|
super.onItemRangeRemoved(positionStart, itemCount)
|
||||||
|
_dataCountState.value = data.size
|
||||||
checkEmpty()
|
checkEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) {
|
override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) {
|
||||||
super.onItemRangeMoved(fromPosition, toPosition, itemCount)
|
super.onItemRangeMoved(fromPosition, toPosition, itemCount)
|
||||||
|
_dataCountState.value = data.size
|
||||||
checkEmpty()
|
checkEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
|
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
|
||||||
super.onItemRangeInserted(positionStart, itemCount)
|
super.onItemRangeInserted(positionStart, itemCount)
|
||||||
|
_dataCountState.value = data.size
|
||||||
checkEmpty()
|
checkEmpty()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -58,7 +73,7 @@ abstract class RecyclerViewAdapter<T>(
|
|||||||
|
|
||||||
private fun checkEmpty() {
|
private fun checkEmpty() {
|
||||||
emptyView ?. let {
|
emptyView ?. let {
|
||||||
if (data.isEmpty()) {
|
if (dataCountState.value == 0) {
|
||||||
it.visibility = View.VISIBLE
|
it.visibility = View.VISIBLE
|
||||||
} else {
|
} else {
|
||||||
it.visibility = View.GONE
|
it.visibility = View.GONE
|
||||||
@@ -66,3 +81,11 @@ abstract class RecyclerViewAdapter<T>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun <T> RecyclerViewAdapter(
|
||||||
|
data: List<T>,
|
||||||
|
onCreateViewHolder: (parent: ViewGroup, viewType: Int) -> AbstractViewHolder<T>
|
||||||
|
) = object : RecyclerViewAdapter<T>() {
|
||||||
|
override val data: List<T> = data
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AbstractViewHolder<T> = onCreateViewHolder(parent, viewType)
|
||||||
|
}
|
||||||
|
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
27
build.gradle
27
build.gradle
@@ -1,6 +1,5 @@
|
|||||||
buildscript {
|
buildscript {
|
||||||
repositories {
|
repositories {
|
||||||
jcenter()
|
|
||||||
google()
|
google()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
mavenLocal()
|
mavenLocal()
|
||||||
@@ -8,22 +7,32 @@ buildscript {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:4.0.2'
|
classpath libs.buildscript.kt.gradle
|
||||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
classpath libs.buildscript.kt.serialization
|
||||||
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
|
classpath libs.buildscript.kt.ksp
|
||||||
classpath "com.getkeepsafe.dexcount:dexcount-gradle-plugin:$dexcount_version"
|
classpath libs.buildscript.jb.dokka
|
||||||
classpath "com.github.breadmoirai:github-release:$github_release_plugin_version"
|
classpath libs.buildscript.gh.release
|
||||||
classpath "org.jetbrains.dokka:dokka-gradle-plugin:$dokka_version"
|
classpath libs.buildscript.android.gradle
|
||||||
|
classpath libs.buildscript.android.dexcount
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
allprojects {
|
allprojects {
|
||||||
repositories {
|
repositories {
|
||||||
mavenLocal()
|
mavenLocal()
|
||||||
jcenter()
|
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
google()
|
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
|
||||||
|
if (it != rootProject.findProject("docs")) {
|
||||||
|
tasks.whenTaskAdded { task ->
|
||||||
|
if(task.name == "jsLegacyBrowserTest" || task.name == "jsLegacyNodeTest") {
|
||||||
|
task.enabled = false
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -5,3 +5,31 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$mppProjectWithSerializationPresetPath"
|
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
|
package dev.inmo.micro_utils.common
|
||||||
|
|
||||||
@RequiresOptIn(
|
@RequiresOptIn(
|
||||||
@@ -12,11 +14,9 @@ package dev.inmo.micro_utils.common
|
|||||||
AnnotationTarget.PROPERTY_GETTER,
|
AnnotationTarget.PROPERTY_GETTER,
|
||||||
AnnotationTarget.PROPERTY_SETTER,
|
AnnotationTarget.PROPERTY_SETTER,
|
||||||
AnnotationTarget.FUNCTION,
|
AnnotationTarget.FUNCTION,
|
||||||
AnnotationTarget.TYPE,
|
AnnotationTarget.TYPEALIAS
|
||||||
AnnotationTarget.TYPEALIAS,
|
|
||||||
AnnotationTarget.TYPE_PARAMETER
|
|
||||||
)
|
)
|
||||||
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(
|
@RequiresOptIn(
|
||||||
"This thing is marked as warned. See message of warn to get more info",
|
"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_GETTER,
|
||||||
AnnotationTarget.PROPERTY_SETTER,
|
AnnotationTarget.PROPERTY_SETTER,
|
||||||
AnnotationTarget.FUNCTION,
|
AnnotationTarget.FUNCTION,
|
||||||
AnnotationTarget.TYPE,
|
AnnotationTarget.TYPEALIAS
|
||||||
AnnotationTarget.TYPEALIAS,
|
|
||||||
AnnotationTarget.TYPE_PARAMETER
|
|
||||||
)
|
)
|
||||||
annotation class Warning(val message: String)
|
annotation class Warning(val message: String)
|
||||||
|
@@ -1,89 +0,0 @@
|
|||||||
package dev.inmo.micro_utils.common
|
|
||||||
|
|
||||||
import kotlin.experimental.and
|
|
||||||
|
|
||||||
private const val BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
|
||||||
private const val BASE64_MASK: Byte = 0x3f
|
|
||||||
private const val BASE64_PAD = '='
|
|
||||||
private val BASE64_INVERSE_ALPHABET = IntArray(256) {
|
|
||||||
BASE64_ALPHABET.indexOf(it.toChar())
|
|
||||||
}
|
|
||||||
|
|
||||||
internal fun Int.toBase64(): Char = BASE64_ALPHABET[this]
|
|
||||||
internal fun Byte.fromBase64(): Byte = BASE64_INVERSE_ALPHABET[toInt() and 0xff].toByte() and BASE64_MASK
|
|
||||||
|
|
||||||
const val replacedInCrypto = "Replaced in crypto subproject"
|
|
||||||
|
|
||||||
@Deprecated(replacedInCrypto, ReplaceWith("EncodedBase64String", "dev.inmo.micro_utils.crypto"))
|
|
||||||
typealias EncodedBase64String = String
|
|
||||||
@Deprecated(replacedInCrypto, ReplaceWith("EncodedByteArray", "dev.inmo.micro_utils.crypto"))
|
|
||||||
typealias EncodedByteArray = ByteArray
|
|
||||||
|
|
||||||
@Deprecated(replacedInCrypto, ReplaceWith("encodeBase64String()", "dev.inmo.micro_utils.crypto"))
|
|
||||||
fun String.encodeBase64String(): EncodedBase64String = encodeToByteArray().encodeBase64String()
|
|
||||||
@Deprecated(replacedInCrypto, ReplaceWith("encodeBase64()", "dev.inmo.micro_utils.crypto"))
|
|
||||||
fun String.encodeBase64(): EncodedByteArray = encodeToByteArray().encodeBase64()
|
|
||||||
|
|
||||||
@Deprecated(replacedInCrypto, ReplaceWith("encodeBase64String()", "dev.inmo.micro_utils.crypto"))
|
|
||||||
fun ByteArray.encodeBase64String(): EncodedBase64String = buildString {
|
|
||||||
var i = 0
|
|
||||||
while (this@encodeBase64String.size > i) {
|
|
||||||
val read = kotlin.math.min(3, this@encodeBase64String.size - i)
|
|
||||||
val data = ByteArray(3) {
|
|
||||||
if (it < read) {
|
|
||||||
this@encodeBase64String[it + i]
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val padSize = (data.size - read) * 8 / 6
|
|
||||||
val chunk = ((data[0].toInt() and 0xFF) shl 16) or
|
|
||||||
((data[1].toInt() and 0xFF) shl 8) or
|
|
||||||
(data[2].toInt() and 0xFF)
|
|
||||||
|
|
||||||
for (index in data.size downTo padSize) {
|
|
||||||
val char = (chunk shr (6 * index)) and BASE64_MASK.toInt()
|
|
||||||
append(char.toBase64())
|
|
||||||
}
|
|
||||||
|
|
||||||
repeat(padSize) { append(BASE64_PAD) }
|
|
||||||
|
|
||||||
i += read
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@Deprecated(replacedInCrypto, ReplaceWith("encodeBase64()", "dev.inmo.micro_utils.crypto"))
|
|
||||||
fun ByteArray.encodeBase64(): EncodedByteArray = encodeBase64String().encodeToByteArray()
|
|
||||||
|
|
||||||
@Deprecated(replacedInCrypto, ReplaceWith("decodeBase64()", "dev.inmo.micro_utils.crypto"))
|
|
||||||
fun EncodedBase64String.decodeBase64() = dropLastWhile { it == BASE64_PAD }.encodeToByteArray().decodeBase64()
|
|
||||||
@Deprecated(replacedInCrypto, ReplaceWith("decodeBase64String()", "dev.inmo.micro_utils.crypto"))
|
|
||||||
fun EncodedBase64String.decodeBase64String() = decodeBase64().decodeToString()
|
|
||||||
|
|
||||||
@Deprecated(replacedInCrypto, ReplaceWith("decodeBase64()", "dev.inmo.micro_utils.crypto"))
|
|
||||||
fun EncodedByteArray.decodeBase64(): ByteArray {
|
|
||||||
val result = mutableListOf<Byte>()
|
|
||||||
val data = ByteArray(4)
|
|
||||||
(0 until size step 4).forEach { i ->
|
|
||||||
var read = 0
|
|
||||||
for (j in 0 until 4) {
|
|
||||||
if (j + i < size) {
|
|
||||||
data[j] = get(j + i)
|
|
||||||
read++
|
|
||||||
} else {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val chunk = data.foldIndexed(0) { index, result, current ->
|
|
||||||
result or (current.fromBase64().toInt() shl ((3 - index) * 6))
|
|
||||||
}
|
|
||||||
for (index in data.size - 2 downTo (data.size - read)) {
|
|
||||||
val origin = (chunk shr (8 * index)) and 0xff
|
|
||||||
result.add(origin.toByte())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result.toByteArray()
|
|
||||||
}
|
|
||||||
@Deprecated(replacedInCrypto, ReplaceWith("decodeBase64String()", "dev.inmo.micro_utils.crypto"))
|
|
||||||
fun EncodedByteArray.decodeBase64String() = decodeBase64().decodeToString()
|
|
@@ -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
|
package dev.inmo.micro_utils.common
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
private inline fun <T> getObject(
|
private inline fun <T> getObject(
|
||||||
additional: MutableList<T>,
|
additional: MutableList<T>,
|
||||||
iterator: Iterator<T>
|
iterator: Iterator<T>
|
||||||
@@ -14,35 +16,49 @@ private inline fun <T> getObject(
|
|||||||
/**
|
/**
|
||||||
* Diff object which contains information about differences between two [Iterable]s
|
* 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
|
* @see calculateDiff
|
||||||
*/
|
*/
|
||||||
|
@Serializable
|
||||||
data class Diff<T> internal constructor(
|
data class Diff<T> internal constructor(
|
||||||
val removed: List<IndexedValue<T>>,
|
val removed: List<@Serializable(IndexedValueSerializer::class) IndexedValue<T>>,
|
||||||
/**
|
/**
|
||||||
* Old-New values pairs
|
* Old-New values pairs
|
||||||
*/
|
*/
|
||||||
val replaced: List<Pair<IndexedValue<T>, IndexedValue<T>>>,
|
val replaced: List<Pair<@Serializable(IndexedValueSerializer::class) IndexedValue<T>, @Serializable(IndexedValueSerializer::class) IndexedValue<T>>>,
|
||||||
val added: List<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(
|
private inline fun <T> performChanges(
|
||||||
potentialChanges: MutableList<Pair<IndexedValue<T>?, IndexedValue<T>?>>,
|
potentialChanges: MutableList<Pair<IndexedValue<T>?, IndexedValue<T>?>>,
|
||||||
additionalsInOld: MutableList<T>,
|
additionsInOld: MutableList<T>,
|
||||||
additionalsInNew: MutableList<T>,
|
additionsInNew: MutableList<T>,
|
||||||
changedList: MutableList<Pair<IndexedValue<T>, IndexedValue<T>>>,
|
changedList: MutableList<Pair<IndexedValue<T>, IndexedValue<T>>>,
|
||||||
removedList: MutableList<IndexedValue<T>>,
|
removedList: MutableList<IndexedValue<T>>,
|
||||||
addedList: MutableList<IndexedValue<T>>,
|
addedList: MutableList<IndexedValue<T>>,
|
||||||
strictComparison: Boolean
|
comparisonFun: (T?, T?) -> Boolean
|
||||||
) {
|
) {
|
||||||
var i = -1
|
var i = -1
|
||||||
val (oldObject, newObject) = potentialChanges.lastOrNull() ?: return
|
val (oldObject, newObject) = potentialChanges.lastOrNull() ?: return
|
||||||
for ((old, new) in potentialChanges.take(potentialChanges.size - 1)) {
|
for ((old, new) in potentialChanges.take(potentialChanges.size - 1)) {
|
||||||
i++
|
i++
|
||||||
val oldOneEqualToNewObject = old ?.value === newObject ?.value || (old ?.value == newObject ?.value && !strictComparison)
|
val oldOneEqualToNewObject = comparisonFun(old ?.value, newObject ?.value)
|
||||||
val newOneEqualToOldObject = new ?.value === oldObject ?.value || (new ?.value == oldObject ?.value && !strictComparison)
|
val newOneEqualToOldObject = comparisonFun(new ?.value, oldObject ?.value)
|
||||||
if (oldOneEqualToNewObject || newOneEqualToOldObject) {
|
if (oldOneEqualToNewObject || newOneEqualToOldObject) {
|
||||||
changedList.addAll(
|
changedList.addAll(
|
||||||
potentialChanges.take(i).mapNotNull {
|
potentialChanges.take(i).mapNotNull {
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
if (it.first != null && it.second != null) it as Pair<IndexedValue<T>, IndexedValue<T>> else null
|
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.first().second ?.let { addedList.add(it) }
|
||||||
newPotentials.drop(1).take(newPotentials.size - 2).forEach { (oldOne, newOne) ->
|
newPotentials.drop(1).take(newPotentials.size - 2).forEach { (oldOne, newOne) ->
|
||||||
addedList.add(newOne!!)
|
addedList.add(newOne!!)
|
||||||
oldOne ?.let { additionalsInOld.add(oldOne.value) }
|
oldOne ?.let { additionsInOld.add(oldOne.value) }
|
||||||
}
|
}
|
||||||
if (newPotentials.size > 1) {
|
if (newPotentials.size > 1) {
|
||||||
newPotentials.last().first ?.value ?.let { additionalsInOld.add(it) }
|
newPotentials.last().first ?.value ?.let { additionsInOld.add(it) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
newOneEqualToOldObject -> {
|
newOneEqualToOldObject -> {
|
||||||
newPotentials.first().first ?.let { removedList.add(it) }
|
newPotentials.first().first ?.let { removedList.add(it) }
|
||||||
newPotentials.drop(1).take(newPotentials.size - 2).forEach { (oldOne, newOne) ->
|
newPotentials.drop(1).take(newPotentials.size - 2).forEach { (oldOne, newOne) ->
|
||||||
removedList.add(oldOne!!)
|
removedList.add(oldOne!!)
|
||||||
newOne ?.let { additionalsInNew.add(newOne.value) }
|
newOne ?.let { additionsInNew.add(newOne.value) }
|
||||||
}
|
}
|
||||||
if (newPotentials.size > 1) {
|
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(
|
fun <T> Iterable<T>.calculateDiff(
|
||||||
other: Iterable<T>,
|
other: Iterable<T>,
|
||||||
strictComparison: Boolean = false
|
comparisonFun: (T?, T?) -> Boolean
|
||||||
): Diff<T> {
|
): Diff<T> {
|
||||||
var i = -1
|
var i = -1
|
||||||
var j = -1
|
var j = -1
|
||||||
@@ -120,27 +136,60 @@ fun <T> Iterable<T>.calculateDiff(
|
|||||||
}
|
}
|
||||||
|
|
||||||
when {
|
when {
|
||||||
oldObject === newObject || (oldObject == newObject && !strictComparison) -> {
|
comparisonFun(oldObject, newObject) -> {
|
||||||
changedObjects.addAll(potentiallyChangedObjects.map { it as Pair<IndexedValue<T>, IndexedValue<T>> })
|
changedObjects.addAll(potentiallyChangedObjects.map {
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
it as Pair<IndexedValue<T>, IndexedValue<T>>
|
||||||
|
})
|
||||||
potentiallyChangedObjects.clear()
|
potentiallyChangedObjects.clear()
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
potentiallyChangedObjects.add(oldObject ?.let { IndexedValue(i, oldObject) } to newObject ?.let { IndexedValue(j, newObject) })
|
potentiallyChangedObjects.add(oldObject ?.let { IndexedValue(i, oldObject) } to newObject ?.let { IndexedValue(j, newObject) })
|
||||||
val previousOldsAdditionsSize = additionalInOld.size
|
val previousOldsAdditionsSize = additionalInOld.size
|
||||||
val previousNewsAdditionsSize = additionalInNew.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)
|
i -= (additionalInOld.size - previousOldsAdditionsSize)
|
||||||
j -= (additionalInNew.size - previousNewsAdditionsSize)
|
j -= (additionalInNew.size - previousNewsAdditionsSize)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
potentiallyChangedObjects.add(null to null)
|
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())
|
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)
|
inline fun <T> StrictDiff(old: Iterable<T>, new: Iterable<T>) = old.calculateDiff(new, true)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -149,3 +198,41 @@ inline fun <T> StrictDiff(old: Iterable<T>, new: Iterable<T>) = old.calculateDif
|
|||||||
inline fun <T> Iterable<T>.calculateStrictDiff(
|
inline fun <T> Iterable<T>.calculateStrictDiff(
|
||||||
other: Iterable<T>
|
other: Iterable<T>
|
||||||
) = calculateDiff(other, strictComparison = true)
|
) = calculateDiff(other, strictComparison = true)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 {
|
||||||
|
for (i in it.removed.indices.sortedDescending()) {
|
||||||
|
removeAt(it.removed[i].index)
|
||||||
|
}
|
||||||
|
it.added.forEach { (i, t) ->
|
||||||
|
add(i, t)
|
||||||
|
}
|
||||||
|
it.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>,
|
||||||
|
comparisonFun: (T?, T?) -> Boolean
|
||||||
|
): Diff<T> = calculateDiff(source, comparisonFun).also {
|
||||||
|
for (i in it.removed.indices.sortedDescending()) {
|
||||||
|
removeAt(it.removed[i].index)
|
||||||
|
}
|
||||||
|
it.added.forEach { (i, t) ->
|
||||||
|
add(i, t)
|
||||||
|
}
|
||||||
|
it.replaced.forEach { (_, new) ->
|
||||||
|
set(new.index, new.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -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
|
import kotlinx.serialization.encoding.Encoder
|
||||||
|
|
||||||
typealias ByteArrayAllocator = () -> ByteArray
|
typealias ByteArrayAllocator = () -> ByteArray
|
||||||
|
typealias SuspendByteArrayAllocator = suspend () -> ByteArray
|
||||||
|
|
||||||
val ByteArray.asAllocator: ByteArrayAllocator
|
val ByteArray.asAllocator: ByteArrayAllocator
|
||||||
get() = { this }
|
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> {
|
object ByteArrayAllocatorSerializer : KSerializer<ByteArrayAllocator> {
|
||||||
private val realSerializer = ByteArraySerializer()
|
private val realSerializer = ByteArraySerializer()
|
||||||
@@ -17,7 +25,7 @@ object ByteArrayAllocatorSerializer : KSerializer<ByteArrayAllocator> {
|
|||||||
|
|
||||||
override fun deserialize(decoder: Decoder): ByteArrayAllocator {
|
override fun deserialize(decoder: Decoder): ByteArrayAllocator {
|
||||||
val bytes = realSerializer.deserialize(decoder)
|
val bytes = realSerializer.deserialize(decoder)
|
||||||
return { bytes }
|
return bytes.asAllocator
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun serialize(encoder: Encoder, value: ByteArrayAllocator) {
|
override fun serialize(encoder: Encoder, value: ByteArrayAllocator) {
|
||||||
|
@@ -0,0 +1,3 @@
|
|||||||
|
package dev.inmo.micro_utils.common
|
||||||
|
|
||||||
|
fun <T> Iterable<T?>.firstNotNull() = first { it != null }!!
|
@@ -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,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
|
@@ -1,33 +0,0 @@
|
|||||||
package dev.inmo.micro_utils.common
|
|
||||||
|
|
||||||
import kotlin.random.Random
|
|
||||||
import kotlin.test.Test
|
|
||||||
import kotlin.test.assertEquals
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class Base64Text {
|
|
||||||
|
|
||||||
val testText = """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.
|
|
||||||
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?"""
|
|
||||||
val base64Text = """TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdCwgc2VkIGRvIGVpdXNtb2QgdGVtcG9yIGluY2lkaWR1bnQgdXQgbGFib3JlIGV0IGRvbG9yZSBtYWduYSBhbGlxdWEuIFV0IGVuaW0gYWQgbWluaW0gdmVuaWFtLCBxdWlzIG5vc3RydWQgZXhlcmNpdGF0aW9uIHVsbGFtY28gbGFib3JpcyBuaXNpIHV0IGFsaXF1aXAgZXggZWEgY29tbW9kbyBjb25zZXF1YXQuIER1aXMgYXV0ZSBpcnVyZSBkb2xvciBpbiByZXByZWhlbmRlcml0IGluIHZvbHVwdGF0ZSB2ZWxpdCBlc3NlIGNpbGx1bSBkb2xvcmUgZXUgZnVnaWF0IG51bGxhIHBhcmlhdHVyLiBFeGNlcHRldXIgc2ludCBvY2NhZWNhdCBjdXBpZGF0YXQgbm9uIHByb2lkZW50LCBzdW50IGluIGN1bHBhIHF1aSBvZmZpY2lhIGRlc2VydW50IG1vbGxpdCBhbmltIGlkIGVzdCBsYWJvcnVtLgpTZWQgdXQgcGVyc3BpY2lhdGlzIHVuZGUgb21uaXMgaXN0ZSBuYXR1cyBlcnJvciBzaXQgdm9sdXB0YXRlbSBhY2N1c2FudGl1bSBkb2xvcmVtcXVlIGxhdWRhbnRpdW0sIHRvdGFtIHJlbSBhcGVyaWFtLCBlYXF1ZSBpcHNhIHF1YWUgYWIgaWxsbyBpbnZlbnRvcmUgdmVyaXRhdGlzIGV0IHF1YXNpIGFyY2hpdGVjdG8gYmVhdGFlIHZpdGFlIGRpY3RhIHN1bnQgZXhwbGljYWJvLiBOZW1vIGVuaW0gaXBzYW0gdm9sdXB0YXRlbSBxdWlhIHZvbHVwdGFzIHNpdCBhc3Blcm5hdHVyIGF1dCBvZGl0IGF1dCBmdWdpdCwgc2VkIHF1aWEgY29uc2VxdXVudHVyIG1hZ25pIGRvbG9yZXMgZW9zIHF1aSByYXRpb25lIHZvbHVwdGF0ZW0gc2VxdWkgbmVzY2l1bnQuIE5lcXVlIHBvcnJvIHF1aXNxdWFtIGVzdCwgcXVpIGRvbG9yZW0gaXBzdW0gcXVpYSBkb2xvciBzaXQgYW1ldCwgY29uc2VjdGV0dXIsIGFkaXBpc2NpIHZlbGl0LCBzZWQgcXVpYSBub24gbnVtcXVhbSBlaXVzIG1vZGkgdGVtcG9yYSBpbmNpZHVudCB1dCBsYWJvcmUgZXQgZG9sb3JlIG1hZ25hbSBhbGlxdWFtIHF1YWVyYXQgdm9sdXB0YXRlbS4gVXQgZW5pbSBhZCBtaW5pbWEgdmVuaWFtLCBxdWlzIG5vc3RydW0gZXhlcmNpdGF0aW9uZW0gdWxsYW0gY29ycG9yaXMgc3VzY2lwaXQgbGFib3Jpb3NhbSwgbmlzaSB1dCBhbGlxdWlkIGV4IGVhIGNvbW1vZGkgY29uc2VxdWF0dXI/IFF1aXMgYXV0ZW0gdmVsIGV1bSBpdXJlIHJlcHJlaGVuZGVyaXQgcXVpIGluIGVhIHZvbHVwdGF0ZSB2ZWxpdCBlc3NlIHF1YW0gbmloaWwgbW9sZXN0aWFlIGNvbnNlcXVhdHVyLCB2ZWwgaWxsdW0gcXVpIGRvbG9yZW0gZXVtIGZ1Z2lhdCBxdW8gdm9sdXB0YXMgbnVsbGEgcGFyaWF0dXI/"""
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun decodeEncode() {
|
|
||||||
val encoded = testText.encodeBase64String()
|
|
||||||
assertEquals(base64Text, encoded)
|
|
||||||
val decoded = encoded.decodeBase64String()
|
|
||||||
assertEquals(testText, decoded)
|
|
||||||
}
|
|
||||||
|
|
||||||
val urlTestText = "https://example.com?isitexample=1"
|
|
||||||
val urlTestBase64Text = "aHR0cHM6Ly9leGFtcGxlLmNvbT9pc2l0ZXhhbXBsZT0x"
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun decodeEncodeUrl() {
|
|
||||||
val encoded = urlTestText.encodeBase64String()
|
|
||||||
assertEquals(urlTestBase64Text, encoded)
|
|
||||||
val decoded = encoded.decodeBase64String()
|
|
||||||
assertEquals(urlTestText, decoded)
|
|
||||||
}
|
|
||||||
}
|
|
@@ -11,7 +11,7 @@ class DiffUtilsTests {
|
|||||||
val withIndex = oldList.withIndex()
|
val withIndex = oldList.withIndex()
|
||||||
|
|
||||||
for (count in 1 .. (floor(oldList.size.toFloat() / 2).toInt())) {
|
for (count in 1 .. (floor(oldList.size.toFloat() / 2).toInt())) {
|
||||||
for ((i, v) in withIndex) {
|
for ((i, _) in withIndex) {
|
||||||
if (i + count > oldList.lastIndex) {
|
if (i + count > oldList.lastIndex) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -32,7 +32,7 @@ class DiffUtilsTests {
|
|||||||
val withIndex = oldList.withIndex()
|
val withIndex = oldList.withIndex()
|
||||||
|
|
||||||
for (count in 1 .. (floor(oldList.size.toFloat() / 2).toInt())) {
|
for (count in 1 .. (floor(oldList.size.toFloat() / 2).toInt())) {
|
||||||
for ((i, v) in withIndex) {
|
for ((i, _) in withIndex) {
|
||||||
if (i + count > oldList.lastIndex) {
|
if (i + count > oldList.lastIndex) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -54,8 +54,8 @@ class DiffUtilsTests {
|
|||||||
val oldList = (0 until 10).map { it.toString() }
|
val oldList = (0 until 10).map { it.toString() }
|
||||||
val withIndex = oldList.withIndex()
|
val withIndex = oldList.withIndex()
|
||||||
|
|
||||||
for (step in 0 until oldList.size) {
|
for (step in oldList.indices) {
|
||||||
for ((i, v) in withIndex) {
|
for ((i, _) in withIndex) {
|
||||||
val mutable = oldList.toMutableList()
|
val mutable = oldList.toMutableList()
|
||||||
val changes = (
|
val changes = (
|
||||||
if (step == 0) i until oldList.size else (i until oldList.size step step)
|
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
|
@@ -33,3 +33,15 @@ fun View.toggleVisibility(goneOnHide: Boolean = true) {
|
|||||||
show()
|
show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun View.changeVisibility(show: Boolean = !isShown, goneOnHide: Boolean = true) {
|
||||||
|
if (show) {
|
||||||
|
show()
|
||||||
|
} else {
|
||||||
|
if (goneOnHide) {
|
||||||
|
gone()
|
||||||
|
} else {
|
||||||
|
hide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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 {
|
sourceSets {
|
||||||
commonMain {
|
commonMain {
|
||||||
dependencies {
|
dependencies {
|
||||||
api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version"
|
api libs.kt.coroutines
|
||||||
|
}
|
||||||
|
}
|
||||||
|
jsMain {
|
||||||
|
dependencies {
|
||||||
|
api project(":micro_utils.common")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
androidMain {
|
androidMain {
|
||||||
dependencies {
|
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)
|
||||||
|
}
|
@@ -0,0 +1,46 @@
|
|||||||
|
package dev.inmo.micro_utils.coroutines
|
||||||
|
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.channels.Channel
|
||||||
|
import kotlin.coroutines.*
|
||||||
|
|
||||||
|
interface ActorAction<T> {
|
||||||
|
suspend operator fun invoke(): T
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Planned to use with [doWithSuspending]. Will execute incoming lambdas sequentially
|
||||||
|
*
|
||||||
|
* @see actor
|
||||||
|
*/
|
||||||
|
fun CoroutineScope.createActionsActor() = actor<suspend () -> Unit> {
|
||||||
|
it()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Planned to use with [doWithSuspending]. Will execute incoming lambdas sequentially
|
||||||
|
*
|
||||||
|
* @see safeActor
|
||||||
|
*/
|
||||||
|
inline fun CoroutineScope.createSafeActionsActor(
|
||||||
|
noinline onException: ExceptionHandler<Unit> = defaultSafelyExceptionHandler
|
||||||
|
) = safeActor<suspend () -> Unit>(Channel.UNLIMITED, onException) {
|
||||||
|
it()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Must be use with actor created by [createActionsActor] or [createSafeActionsActor]. Will send lambda which will
|
||||||
|
* execute [action] and return result.
|
||||||
|
*
|
||||||
|
* @see suspendCoroutine
|
||||||
|
* @see safely
|
||||||
|
*/
|
||||||
|
suspend fun <T> Channel<suspend () -> Unit>.doWithSuspending(
|
||||||
|
action: ActorAction<T>
|
||||||
|
) = suspendCoroutine<T> {
|
||||||
|
trySend {
|
||||||
|
safely({ e -> it.resumeWithException(e) }) {
|
||||||
|
it.resume(action())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -1,19 +1,15 @@
|
|||||||
package dev.inmo.micro_utils.coroutines
|
package dev.inmo.micro_utils.coroutines
|
||||||
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.flow.consumeAsFlow
|
||||||
|
|
||||||
fun <T> CoroutineScope.actor(
|
fun <T> CoroutineScope.actor(
|
||||||
channelCapacity: Int = Channel.UNLIMITED,
|
channelCapacity: Int = Channel.UNLIMITED,
|
||||||
block: suspend (T) -> Unit
|
block: suspend (T) -> Unit
|
||||||
): Channel<T> {
|
): Channel<T> {
|
||||||
val channel = Channel<T>(channelCapacity)
|
val channel = Channel<T>(channelCapacity)
|
||||||
launch {
|
channel.consumeAsFlow().subscribe(this, block)
|
||||||
for (data in channel) {
|
|
||||||
block(data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return channel
|
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(
|
suspend fun <T> Iterable<Deferred<T>>.awaitFirstWithDeferred(
|
||||||
scope: CoroutineScope,
|
scope: CoroutineScope,
|
||||||
cancelOnResult: Boolean = true
|
cancelOnResult: Boolean = true
|
||||||
): Pair<Deferred<T>, T> = suspendCoroutine<Pair<Deferred<T>, T>> { continuation ->
|
): Pair<Deferred<T>, T> {
|
||||||
scope.launch(SupervisorJob()) {
|
val resultDeferred = CompletableDeferred<Pair<Deferred<T>, T>>()
|
||||||
val scope = this
|
val scope = scope.LinkedSupervisorScope()
|
||||||
forEach {
|
forEach {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
continuation.resume(it to it.await())
|
resultDeferred.complete(it to it.await())
|
||||||
scope.cancel()
|
scope.cancel()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.also {
|
return resultDeferred.await().also {
|
||||||
if (cancelOnResult) {
|
if (cancelOnResult) {
|
||||||
forEach {
|
forEach {
|
||||||
try {
|
runCatchingSafely { it.cancel() }
|
||||||
it.cancel()
|
|
||||||
} catch (e: IllegalStateException) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,23 @@
|
|||||||
|
package dev.inmo.micro_utils.coroutines
|
||||||
|
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
|
||||||
|
inline val UI
|
||||||
|
get() = Dispatchers.Main
|
||||||
|
inline val Default
|
||||||
|
get() = Dispatchers.Default
|
||||||
|
|
||||||
|
suspend inline fun <T> doIn(context: CoroutineContext, noinline block: suspend CoroutineScope.() -> T) = withContext(
|
||||||
|
context,
|
||||||
|
block
|
||||||
|
)
|
||||||
|
|
||||||
|
suspend inline fun <T> doInUI(noinline block: suspend CoroutineScope.() -> T) = doIn(
|
||||||
|
UI,
|
||||||
|
block
|
||||||
|
)
|
||||||
|
suspend inline fun <T> doInDefault(noinline block: suspend CoroutineScope.() -> T) = doIn(
|
||||||
|
Default,
|
||||||
|
block
|
||||||
|
)
|
@@ -9,6 +9,19 @@ class DeferredAction<T, O>(
|
|||||||
suspend operator fun invoke() = callback(deferred.await())
|
suspend operator fun invoke() = callback(deferred.await())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class DoWithFirstBuilder<T>(
|
||||||
|
private val scope: CoroutineScope
|
||||||
|
) {
|
||||||
|
private val deferreds = mutableListOf<Deferred<T>>()
|
||||||
|
operator fun plus(block: suspend CoroutineScope.() -> T) {
|
||||||
|
deferreds.add(scope.async(start = CoroutineStart.LAZY, block = block))
|
||||||
|
}
|
||||||
|
inline fun add(noinline block: suspend CoroutineScope.() -> T) = plus(block)
|
||||||
|
inline fun include(noinline block: suspend CoroutineScope.() -> T) = plus(block)
|
||||||
|
|
||||||
|
fun build() = deferreds.toList()
|
||||||
|
}
|
||||||
|
|
||||||
fun <T, O> Deferred<T>.buildAction(callback: suspend (T) -> O) = DeferredAction(this, callback)
|
fun <T, O> Deferred<T>.buildAction(callback: suspend (T) -> O) = DeferredAction(this, callback)
|
||||||
|
|
||||||
suspend fun <O> Iterable<DeferredAction<*, O>>.invokeFirstOf(
|
suspend fun <O> Iterable<DeferredAction<*, O>>.invokeFirstOf(
|
||||||
@@ -32,9 +45,41 @@ suspend fun <T, O> Iterable<Deferred<T>>.invokeOnFirst(
|
|||||||
callback: suspend (T) -> O
|
callback: suspend (T) -> O
|
||||||
): O = map { it.buildAction(callback) }.invokeFirstOf(scope, cancelOnResult)
|
): O = map { it.buildAction(callback) }.invokeFirstOf(scope, cancelOnResult)
|
||||||
|
|
||||||
|
suspend fun <T, O> CoroutineScope.invokeOnFirstOf(
|
||||||
|
cancelOnResult: Boolean = true,
|
||||||
|
block: DoWithFirstBuilder<T>.() -> Unit,
|
||||||
|
callback: suspend (T) -> O
|
||||||
|
) = firstOf(
|
||||||
|
DoWithFirstBuilder<T>(this).apply(block).build(),
|
||||||
|
cancelOnResult
|
||||||
|
).let { callback(it) }
|
||||||
|
|
||||||
suspend fun <T, O> invokeOnFirst(
|
suspend fun <T, O> invokeOnFirst(
|
||||||
scope: CoroutineScope,
|
scope: CoroutineScope,
|
||||||
vararg variants: Deferred<T>,
|
vararg variants: Deferred<T>,
|
||||||
cancelOnResult: Boolean = true,
|
cancelOnResult: Boolean = true,
|
||||||
callback: suspend (T) -> O
|
callback: suspend (T) -> O
|
||||||
): O = variants.toList().invokeOnFirst(scope, cancelOnResult, callback)
|
): O = variants.toList().invokeOnFirst(scope, cancelOnResult, callback)
|
||||||
|
|
||||||
|
suspend fun <T> CoroutineScope.firstOf(
|
||||||
|
variants: Iterable<Deferred<T>>,
|
||||||
|
cancelOnResult: Boolean = true
|
||||||
|
) = variants.invokeOnFirst(this, cancelOnResult) { it }
|
||||||
|
|
||||||
|
suspend fun <T> CoroutineScope.firstOf(
|
||||||
|
cancelOnResult: Boolean = true,
|
||||||
|
block: DoWithFirstBuilder<T>.() -> Unit
|
||||||
|
) = firstOf(
|
||||||
|
DoWithFirstBuilder<T>(this).apply(block).build(),
|
||||||
|
cancelOnResult
|
||||||
|
)
|
||||||
|
|
||||||
|
suspend fun <T> CoroutineScope.firstOf(
|
||||||
|
vararg variants: Deferred<T>,
|
||||||
|
cancelOnResult: Boolean = true
|
||||||
|
) = firstOf(variants.toList(), cancelOnResult)
|
||||||
|
|
||||||
|
suspend fun <T> List<Deferred<T>>.first(
|
||||||
|
scope: CoroutineScope,
|
||||||
|
cancelOnResult: Boolean = true
|
||||||
|
) = scope.firstOf(this, cancelOnResult)
|
||||||
|
@@ -0,0 +1,6 @@
|
|||||||
|
package dev.inmo.micro_utils.coroutines
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
|
||||||
|
suspend fun <T> Flow<T?>.firstNotNull() = first { it != null }!!
|
@@ -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.CoroutineScope
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shortcut for chain if [Flow.onEach] and [Flow.launchIn]
|
* Shortcut for chain if [Flow.onEach] and [Flow.launchIn]
|
||||||
@@ -25,13 +27,26 @@ inline fun <T> Flow<T>.subscribeSafely(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Use [subscribeSafelyWithoutExceptions], but all exceptions inside of [safely] will be skipped
|
* Use [subscribeSafelyWithoutExceptions], but all exceptions will be passed to [defaultSafelyExceptionHandler]
|
||||||
*/
|
*/
|
||||||
inline fun <T> Flow<T>.subscribeSafelyWithoutExceptions(
|
inline fun <T> Flow<T>.subscribeSafelyWithoutExceptions(
|
||||||
scope: CoroutineScope,
|
scope: CoroutineScope,
|
||||||
|
noinline onException: ExceptionHandler<T?> = defaultSafelyWithoutExceptionHandlerWithNull,
|
||||||
noinline block: suspend (T) -> Unit
|
noinline block: suspend (T) -> Unit
|
||||||
) = subscribeSafely(
|
) = subscribe(scope) {
|
||||||
scope,
|
safelyWithoutExceptions(onException) {
|
||||||
{},
|
block(it)
|
||||||
block
|
}
|
||||||
)
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use [subscribeSafelyWithoutExceptions], but all exceptions inside of [safely] will be skipped
|
||||||
|
*/
|
||||||
|
inline fun <T> Flow<T>.subscribeSafelySkippingExceptions(
|
||||||
|
scope: CoroutineScope,
|
||||||
|
noinline block: suspend (T) -> Unit
|
||||||
|
) = subscribe(scope) {
|
||||||
|
safelyWithoutExceptions({ /* skip exceptions */ }) {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
@@ -22,34 +22,6 @@ var defaultSafelyWithoutExceptionHandler: ExceptionHandler<Unit> = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Key for [SafelyExceptionHandler] which can be used in [CoroutineContext.get] to get current default
|
|
||||||
* [SafelyExceptionHandler]
|
|
||||||
*/
|
|
||||||
@Deprecated("This method will be useless in future major update", ReplaceWith("ContextSafelyExceptionHandlerKey", "dev.inmo.micro_utils.coroutines.ContextSafelyExceptionHandler"))
|
|
||||||
class SafelyExceptionHandlerKey<T> : CoroutineContext.Key<SafelyExceptionHandler<T>>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shortcut for creating instance of [SafelyExceptionHandlerKey]
|
|
||||||
*/
|
|
||||||
@Suppress("NOTHING_TO_INLINE")
|
|
||||||
@Deprecated("This method will be useless in future major update", ReplaceWith("ContextSafelyExceptionHandlerKey", "dev.inmo.micro_utils.coroutines.ContextSafelyExceptionHandler"))
|
|
||||||
inline fun <T> safelyExceptionHandlerKey() = SafelyExceptionHandlerKey<T>()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wrapper for [ExceptionHandler] which can be used in [CoroutineContext] to set local (for [CoroutineContext]) default
|
|
||||||
* [ExceptionHandler]. To get it use [CoroutineContext.get] with key [SafelyExceptionHandlerKey]
|
|
||||||
*
|
|
||||||
* @see SafelyExceptionHandlerKey
|
|
||||||
* @see ExceptionHandler
|
|
||||||
*/
|
|
||||||
@Deprecated("This method will be useless in future major update", ReplaceWith("ContextSafelyExceptionHandler", "dev.inmo.micro_utils.coroutines.ContextSafelyExceptionHandler"))
|
|
||||||
class SafelyExceptionHandler<T>(
|
|
||||||
val handler: ExceptionHandler<T>
|
|
||||||
) : CoroutineContext.Element {
|
|
||||||
override val key: CoroutineContext.Key<*> = safelyExceptionHandlerKey<T>()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This key can (and will) be used to get [ContextSafelyExceptionHandler] from [coroutineContext] of suspend functions
|
* This key can (and will) be used to get [ContextSafelyExceptionHandler] from [coroutineContext] of suspend functions
|
||||||
* and in [ContextSafelyExceptionHandler] for defining of its [CoroutineContext.Element.key]
|
* and in [ContextSafelyExceptionHandler] for defining of its [CoroutineContext.Element.key]
|
||||||
@@ -114,6 +86,9 @@ suspend fun <T> safelyWithContextExceptionHandler(
|
|||||||
* * [CoroutineContext.get] with [SafelyExceptionHandlerKey] as key
|
* * [CoroutineContext.get] with [SafelyExceptionHandlerKey] as key
|
||||||
* * [defaultSafelyExceptionHandler]
|
* * [defaultSafelyExceptionHandler]
|
||||||
*
|
*
|
||||||
|
* Remember, that [ExceptionHandler] from [CoroutineContext.get] will be used anyway if it is available. After it will
|
||||||
|
* be called [onException]
|
||||||
|
*
|
||||||
* @param [onException] Will be called when happen exception inside of [block]. By default will throw exception - this
|
* @param [onException] Will be called when happen exception inside of [block]. By default will throw exception - this
|
||||||
* exception will be available for catching
|
* exception will be available for catching
|
||||||
*
|
*
|
||||||
@@ -133,24 +108,60 @@ suspend inline fun <T> safely(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend inline fun <T> runCatchingSafely(
|
||||||
|
noinline onException: ExceptionHandler<T> = defaultSafelyExceptionHandler,
|
||||||
|
noinline block: suspend CoroutineScope.() -> T
|
||||||
|
): Result<T> = runCatching {
|
||||||
|
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
|
||||||
|
*
|
||||||
|
* @see safelyWithoutExceptions
|
||||||
|
* @see launchSafelyWithoutExceptions
|
||||||
|
* @see asyncSafelyWithoutExceptions
|
||||||
|
*/
|
||||||
|
val defaultSafelyWithoutExceptionHandlerWithNull: ExceptionHandler<Nothing?> = {
|
||||||
|
defaultSafelyWithoutExceptionHandler.invoke(it)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shortcut for [safely] with exception handler, that as expected must return null in case of impossible creating of
|
* Shortcut for [safely] with exception handler, that as expected must return null in case of impossible creating of
|
||||||
* result from exception (instead of throwing it)
|
* result from exception (instead of throwing it, by default always returns null)
|
||||||
*/
|
*/
|
||||||
suspend inline fun <T> safelyWithoutExceptions(
|
suspend inline fun <T> safelyWithoutExceptions(
|
||||||
noinline onException: ExceptionHandler<T?>,
|
noinline onException: ExceptionHandler<T?> = defaultSafelyWithoutExceptionHandlerWithNull,
|
||||||
noinline block: suspend CoroutineScope.() -> T
|
noinline block: suspend CoroutineScope.() -> T
|
||||||
): T? = safely(onException, block)
|
): T? = safely(onException, block)
|
||||||
|
|
||||||
/**
|
suspend inline fun <T> runCatchingSafelyWithoutExceptions(
|
||||||
* Shortcut for [safely] without exception handler (instead of this you will always receive null as a result)
|
noinline onException: ExceptionHandler<T?> = defaultSafelyWithoutExceptionHandlerWithNull,
|
||||||
*/
|
|
||||||
suspend inline fun <T> safelyWithoutExceptions(
|
|
||||||
noinline block: suspend CoroutineScope.() -> T
|
noinline block: suspend CoroutineScope.() -> T
|
||||||
): T? = safelyWithoutExceptions(
|
): Result<T?> = runCatching {
|
||||||
{
|
safelyWithoutExceptions(onException, block)
|
||||||
defaultSafelyWithoutExceptionHandler.invoke(it)
|
}
|
||||||
null
|
|
||||||
},
|
inline fun CoroutineScope(
|
||||||
block
|
context: CoroutineContext,
|
||||||
|
noinline defaultExceptionsHandler: ExceptionHandler<Unit>
|
||||||
|
) = CoroutineScope(
|
||||||
|
context + ContextSafelyExceptionHandler(defaultExceptionsHandler)
|
||||||
)
|
)
|
||||||
|
@@ -0,0 +1,41 @@
|
|||||||
|
package dev.inmo.micro_utils.coroutines
|
||||||
|
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
import kotlin.coroutines.EmptyCoroutineContext
|
||||||
|
|
||||||
|
inline fun CoroutineScope.launchSafely(
|
||||||
|
context: CoroutineContext = EmptyCoroutineContext,
|
||||||
|
start: CoroutineStart = CoroutineStart.DEFAULT,
|
||||||
|
noinline onException: ExceptionHandler<Unit> = defaultSafelyExceptionHandler,
|
||||||
|
noinline block: suspend CoroutineScope.() -> Unit
|
||||||
|
) = launch(context, start) {
|
||||||
|
safely(onException, block)
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun CoroutineScope.launchSafelyWithoutExceptions(
|
||||||
|
context: CoroutineContext = EmptyCoroutineContext,
|
||||||
|
start: CoroutineStart = CoroutineStart.DEFAULT,
|
||||||
|
noinline onException: ExceptionHandler<Unit?> = defaultSafelyWithoutExceptionHandlerWithNull,
|
||||||
|
noinline block: suspend CoroutineScope.() -> Unit
|
||||||
|
) = launch(context, start) {
|
||||||
|
safelyWithoutExceptions(onException, block)
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <T> CoroutineScope.asyncSafely(
|
||||||
|
context: CoroutineContext = EmptyCoroutineContext,
|
||||||
|
start: CoroutineStart = CoroutineStart.DEFAULT,
|
||||||
|
noinline onException: ExceptionHandler<T> = defaultSafelyExceptionHandler,
|
||||||
|
noinline block: suspend CoroutineScope.() -> T
|
||||||
|
) = async(context, start) {
|
||||||
|
safely(onException, block)
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <T> CoroutineScope.asyncSafelyWithoutExceptions(
|
||||||
|
context: CoroutineContext = EmptyCoroutineContext,
|
||||||
|
start: CoroutineStart = CoroutineStart.DEFAULT,
|
||||||
|
noinline onException: ExceptionHandler<T?> = defaultSafelyWithoutExceptionHandlerWithNull,
|
||||||
|
noinline block: suspend CoroutineScope.() -> T
|
||||||
|
) = async(context, start) {
|
||||||
|
safelyWithoutExceptions(onException, block)
|
||||||
|
}
|
@@ -0,0 +1,17 @@
|
|||||||
|
package dev.inmo.micro_utils.coroutines
|
||||||
|
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
|
||||||
|
fun CoroutineContext.LinkedSupervisorJob(
|
||||||
|
additionalContext: CoroutineContext? = null
|
||||||
|
) = SupervisorJob(job).let { if (additionalContext != null) it + additionalContext else it }
|
||||||
|
fun CoroutineScope.LinkedSupervisorJob(
|
||||||
|
additionalContext: CoroutineContext? = null
|
||||||
|
) = coroutineContext.LinkedSupervisorJob(additionalContext)
|
||||||
|
|
||||||
|
fun CoroutineScope.LinkedSupervisorScope(
|
||||||
|
additionalContext: CoroutineContext? = null
|
||||||
|
) = CoroutineScope(
|
||||||
|
coroutineContext + LinkedSupervisorJob(additionalContext)
|
||||||
|
)
|
@@ -0,0 +1,31 @@
|
|||||||
|
package dev.inmo.micro_utils.coroutines
|
||||||
|
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
import kotlin.coroutines.EmptyCoroutineContext
|
||||||
|
|
||||||
|
private fun CoroutineScope.createWeakSubScope() = CoroutineScope(coroutineContext.minusKey(Job)).also { newScope ->
|
||||||
|
coroutineContext.job.invokeOnCompletion { newScope.cancel() }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun CoroutineScope.weakLaunch(
|
||||||
|
context: CoroutineContext = EmptyCoroutineContext,
|
||||||
|
start: CoroutineStart = CoroutineStart.DEFAULT,
|
||||||
|
block: suspend CoroutineScope.() -> Unit
|
||||||
|
): Job {
|
||||||
|
val scope = createWeakSubScope()
|
||||||
|
val job = scope.launch(context, start, block)
|
||||||
|
job.invokeOnCompletion { scope.cancel() }
|
||||||
|
return job
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> CoroutineScope.weakAsync(
|
||||||
|
context: CoroutineContext = EmptyCoroutineContext,
|
||||||
|
start: CoroutineStart = CoroutineStart.DEFAULT,
|
||||||
|
block: suspend CoroutineScope.() -> T
|
||||||
|
): Deferred<T> {
|
||||||
|
val scope = createWeakSubScope()
|
||||||
|
val deferred = scope.async(context, start, block)
|
||||||
|
deferred.invokeOnCompletion { scope.cancel() }
|
||||||
|
return deferred
|
||||||
|
}
|
@@ -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,11 @@
|
|||||||
|
package dev.inmo.micro_utils.coroutines
|
||||||
|
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
|
||||||
|
val IO
|
||||||
|
get() = Dispatchers.IO
|
||||||
|
|
||||||
|
suspend inline fun <T> doInIO(noinline block: suspend CoroutineScope.() -> T) = doIn(
|
||||||
|
IO,
|
||||||
|
block
|
||||||
|
)
|
@@ -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)
|
||||||
|
}
|
@@ -3,28 +3,24 @@ package dev.inmo.micro_utils.coroutines
|
|||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
|
|
||||||
fun <T> CoroutineScope.launchSynchronously(block: suspend CoroutineScope.() -> T): T {
|
fun <T> CoroutineScope.launchSynchronously(block: suspend CoroutineScope.() -> T): T {
|
||||||
var throwable: Throwable? = null
|
var result: Result<T>? = null
|
||||||
var result: T? = null
|
val objectToSynchronize = Object()
|
||||||
val objectToSynchronize = java.lang.Object()
|
synchronized(objectToSynchronize) {
|
||||||
val launchCallback = {
|
launch(start = CoroutineStart.UNDISPATCHED) {
|
||||||
launch {
|
result = safelyWithResult(block)
|
||||||
safely(
|
}.invokeOnCompletion {
|
||||||
{
|
|
||||||
throwable = it
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
result = block()
|
|
||||||
}
|
|
||||||
synchronized(objectToSynchronize) {
|
synchronized(objectToSynchronize) {
|
||||||
objectToSynchronize.notifyAll()
|
objectToSynchronize.notifyAll()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
while (result == null) {
|
||||||
|
objectToSynchronize.wait()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
synchronized(objectToSynchronize) {
|
return result!!.getOrThrow()
|
||||||
launchCallback()
|
|
||||||
objectToSynchronize.wait()
|
|
||||||
}
|
|
||||||
throw throwable ?: return result!!
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun <T> launchSynchronously(block: suspend CoroutineScope.() -> T): T = CoroutineScope(Dispatchers.Default).launchSynchronously(block)
|
fun <T> launchSynchronously(block: suspend CoroutineScope.() -> T): T = CoroutineScope(Dispatchers.Default).launchSynchronously(block)
|
||||||
|
|
||||||
|
fun <T> CoroutineScope.doSynchronously(block: suspend CoroutineScope.() -> T): T = launchSynchronously(block)
|
||||||
|
fun <T> doSynchronously(block: suspend CoroutineScope.() -> T): T = launchSynchronously(block)
|
||||||
|
@@ -1,9 +1,8 @@
|
|||||||
package dev.inmo.micro_utils.coroutines
|
package dev.inmo.micro_utils.coroutines
|
||||||
|
|
||||||
import dev.inmo.micro_utils.coroutines.asDeferred
|
|
||||||
import dev.inmo.micro_utils.coroutines.launchSynchronously
|
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlin.test.*
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
class DoWithFirstTests {
|
class DoWithFirstTests {
|
||||||
@Test
|
@Test
|
||||||
|
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,40 @@
|
|||||||
|
package dev.inmo.micro_utils.coroutines
|
||||||
|
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class WeakJob {
|
||||||
|
@Test
|
||||||
|
fun `test that weak jobs works correctly`() {
|
||||||
|
val scope = CoroutineScope(Dispatchers.Default)
|
||||||
|
lateinit var weakLaunchJob: Job
|
||||||
|
lateinit var weakAsyncJob: Job
|
||||||
|
scope.launchSynchronously {
|
||||||
|
val completeDeferred = Job()
|
||||||
|
coroutineScope {
|
||||||
|
weakLaunchJob = weakLaunch {
|
||||||
|
while (isActive) {
|
||||||
|
delay(100L)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
weakAsyncJob = weakAsync {
|
||||||
|
while (isActive) {
|
||||||
|
delay(100L)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
coroutineContext.job.invokeOnCompletion {
|
||||||
|
scope.launch {
|
||||||
|
delay(1000L)
|
||||||
|
completeDeferred.complete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
launch { delay(1000L); cancel() }
|
||||||
|
}
|
||||||
|
completeDeferred.join()
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(!weakLaunchJob.isActive)
|
||||||
|
assert(!weakAsyncJob.isActive)
|
||||||
|
}
|
||||||
|
}
|
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)
|
||||||
|
}
|
||||||
|
}
|
@@ -1,10 +0,0 @@
|
|||||||
package dev.inmo.micro_utils.coroutines
|
|
||||||
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
|
|
||||||
suspend inline fun <T> doInUI(noinline block: suspend CoroutineScope.() -> T) = withContext(
|
|
||||||
Dispatchers.Main,
|
|
||||||
block
|
|
||||||
)
|
|
@@ -11,6 +11,7 @@ kotlin {
|
|||||||
commonMain {
|
commonMain {
|
||||||
dependencies {
|
dependencies {
|
||||||
api project(":micro_utils.common")
|
api project(":micro_utils.common")
|
||||||
|
api libs.krypto
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
jsMain {
|
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()
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user