mirror of
				https://github.com/InsanusMokrassar/MicroUtils.git
				synced 2025-10-26 17:50:41 +00:00 
			
		
		
		
	Compare commits
	
		
			751 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 1bd671685b | |||
| 48d3fe41f2 | |||
| 7ab21871cd | |||
| 65d01b1fb3 | |||
| 6230accb68 | |||
| 10e03bb951 | |||
| aa4f392948 | |||
| f51b59ec02 | |||
| 8c76834ae4 | |||
| 4a454f3d67 | |||
| 151aa1863d | |||
| 3bf6896296 | |||
| 0d01561476 | |||
| f6ded92251 | |||
| d01b735cc6 | |||
| 6c12001080 | |||
| 1afbf03606 | |||
| f6ef5c61c5 | |||
| c18fee8107 | |||
| d9df7a4384 | |||
| 87c2230e8e | |||
| da7eb6de0a | |||
| ed3815118f | |||
| be726f42bd | |||
| a91006132f | |||
| 9a9f741a0b | |||
| 5028f130e9 | |||
| 77fa019651 | |||
| 9715da9384 | |||
| f6d5035c1a | |||
| 43e782ab6f | |||
| f3f9920bfb | |||
| 2bfd615812 | |||
| ebfacb3659 | |||
| c71d557eec | |||
| e0398cef21 | |||
| f91599e9c6 | |||
| f8f9f93c97 | |||
| a8a5340d8b | |||
| 871b27f37d | |||
| 6f174cae1d | |||
| 22d7ac3e22 | |||
| 9b30c3a155 | |||
| 915bac64b1 | |||
| 9d2b50e55d | |||
| bde100f63d | |||
| 05b035a13d | |||
| eefb56bed7 | |||
| fcc0dc4189 | |||
| 47ff20317f | |||
| 1558b9103d | |||
| 7a78742162 | |||
| c01e240f66 | |||
| fef4fcbac6 | |||
| 5ab18bce4b | |||
| 24aec7271a | |||
| 9b19a2cb95 | |||
| efdd7b8a57 | |||
| 6df4cc9c3b | |||
| b9d93db0f5 | |||
| d7ee45ca64 | |||
| d7c31b1b22 | |||
| 7d6794a358 | |||
| 473eb87346 | |||
| 8b18b07790 | |||
| ab3c80a5ec | |||
| 075b93ecd6 | |||
| f6d0f72e49 | |||
| fcda3af862 | |||
| d5c7a589b1 | |||
| 86e099ed25 | |||
| ebd7befe73 | |||
| b8c7e581a1 | |||
| 8281259179 | |||
| d3e06b07df | |||
| 4967018418 | |||
| 537a3c38fa | |||
| 0124957833 | |||
| f0420e2d61 | |||
| 7090566041 | |||
| f0d5035cd0 | |||
| 0fb9b8dc30 | |||
| eef6e81134 | |||
| 1593159a3f | |||
| 3da9eb9dbe | |||
| f17613f3fb | |||
| 14337ccb46 | |||
| 1a3913b09c | |||
| 039aed2747 | |||
| 173991e3cb | |||
| 8b3f8cab01 | |||
| 2a20d24589 | |||
| 53c2d552ec | |||
| af11c1a83d | |||
| a65cf1481c | |||
| 0318716236 | |||
| 88eb4b3342 | |||
| 4810d1ef6a | |||
| f2a9514d89 | |||
| 925ba6ac24 | |||
|  | ef407268a2 | ||
| cd6c4bbe38 | |||
| c058e18408 | |||
| 6d3ca565ca | |||
| 236a7b4fd2 | |||
| e1f387dbf7 | |||
| 3d113dd31e | |||
| e0e57f0336 | |||
| e775b58d41 | |||
| 5b070a8478 | |||
| 8a61193500 | |||
| fad522b8fe | |||
| 0acac205af | |||
| 069d4c61b7 | |||
| 7c113f5700 | |||
| dfdaf4225b | |||
| bd39ab2467 | |||
| 6ce1eb3f2d | |||
| ce7d4fe9a2 | |||
| a65bb2f419 | |||
| cbc868448b | |||
| 9c336a0b56 | |||
| 0f0d09399e | |||
| e13a1162a9 | |||
| 57ebed903f | |||
| 4478193d8a | |||
| ee948395e3 | |||
| 0616b051ae | |||
| 4d155d0505 | |||
| a169e733d9 | |||
| f081e237c8 | |||
| f412d387fa | |||
| 67354b43e2 | |||
| 39135a4000 | |||
| eaa014cebd | |||
| 856e657f81 | |||
| 3a609e5b66 | |||
| d0022dd599 | |||
| 7ba6eed453 | |||
| beeb6ecc0a | |||
| 7cdc17a714 | |||
| 4765a950a9 | |||
| 65e8137e08 | |||
| ae546dd9ad | |||
| 8110c42be0 | |||
| bd2b5ae5fc | |||
| 2ddfffa6a9 | |||
| a4b54e861d | |||
| c6785f1a4f | |||
| 83fe621c56 | |||
| b3a93e17eb | |||
| 546a391af3 | |||
| 786cf9bd8b | |||
| dfd6fe062d | |||
| b6ef818613 | |||
| b0f9e9c30a | |||
| 7e5c88ddc3 | |||
| 9824c3e00f | |||
| 9171d5ed11 | |||
| a1830ebb82 | |||
| 22d2a3d9bf | |||
| c9b97fc965 | |||
| 51f85becd5 | |||
| a8a281cfb4 | |||
| ddd1304949 | |||
| 86d70b6c02 | |||
| a22bdb39e7 | |||
| 7ae4d5ef95 | |||
| a2038cbefa | |||
| 992091eade | |||
| e3bfead0c5 | |||
| 0de96141fd | |||
| fa18d15c3c | |||
| ea9dbf2371 | |||
| d34e3ec7a9 | |||
| c8833a36af | |||
| a067cb0c0f | |||
| 999c8327bd | |||
| c2ec73c70a | |||
| 702f782fc1 | |||
| 25dbcaaf83 | |||
| b00d454a24 | |||
| dbc921d56d | |||
| 438fefa7a3 | |||
| 5d74eac814 | |||
| 9fb62e1e25 | |||
| 3e366ea73b | |||
| 0ff895bffa | |||
| c5bb120280 | |||
| 4b26a92b37 | |||
| 0a8453b4d2 | |||
| c9872a61b6 | |||
| 149a1aa278 | |||
| 13d0e1b682 | |||
| 6f9be2a9f8 | |||
| 93ba98d993 | |||
| 940ee3df06 | |||
| 2e7bab10fd | |||
|  | 3ed70a37ea | ||
| fe8f80b9d9 | |||
| d81fb32fb9 | |||
| 2877b5532c | |||
| b938b21395 | |||
| 58836359cc | |||
| 5edb0e1331 | |||
| 0f0d0b5d58 | |||
| 46c1887cbe | |||
| 5f231c2212 | |||
| 4e97ce86aa | |||
| 315a7cb29e | |||
| aa7cc503f2 | |||
| 4bbe7e5a80 | |||
| d9c05f38d2 | |||
| cd0c4c9650 | |||
| fc3407f104 | |||
| 3a5544206b | |||
| e17e2f7fb8 | |||
|  | d32c95f143 | ||
| 6d8a8ab018 | |||
| a7dce8fa78 | |||
| ca73ff8e19 | |||
| d01ad10d7d | |||
| 81041ee43c | |||
|  | 6e004c2ae4 | ||
| 0e2fac5b22 | |||
| 269da7f155 | |||
| 3cb6b73ee0 | |||
| a938ee1efb | |||
| 6ea5e2e5a6 | |||
| 617dfb54e0 | |||
| d23e005985 | |||
| e5207f5bc5 | |||
| c96cea8db0 | |||
| 0a8e71d76a | |||
| cf1fd32b08 | |||
| cc4224ea1f | |||
| f4c148bc58 | |||
| 022297ad3f | |||
| 5180d6fc3e | |||
| eeebbff70d | |||
| afc6aeea15 | |||
| 486515eddd | |||
| 0e21699cd1 | |||
| f1678ef7cf | |||
| cea65fc76e | |||
| c9e320b72a | |||
| 555956087d | |||
| b3f468f901 | |||
| f5f7511781 | |||
| 4be1d93f60 | |||
| 7d684608ef | |||
| 2c7fd320eb | |||
| 88ee82e1c6 | |||
| d6402c624e | |||
| 8b9c93bc10 | |||
| 4f5e261d01 | |||
| cf455aebe6 | |||
| 1380d5f8e1 | |||
| 5f65260bfe | |||
| 11f0dcfc01 | |||
| 555b7886a4 | |||
| 3707a6c0ce | |||
| 4c8d92b4c3 | |||
| 8bbd33c896 | |||
| ac33a3580f | |||
| a64a32fbe6 | |||
| 9493e97a38 | |||
| 88bd770260 | |||
| a7bd33b7bf | |||
| 73c724a2e5 | |||
| d8cf3c6726 | |||
| 15dee238b5 | |||
| c70626734e | |||
| 5a765ea1bc | |||
| 8215f9d2c6 | |||
| d2e6d2ec80 | |||
| 3718181e8f | |||
| 0d825cf424 | |||
| 28a804d988 | |||
| 9e4bb9d678 | |||
| 9c40d7da3d | |||
| 2b76ad0aa9 | |||
| e4b619e050 | |||
| 36c09feaf2 | |||
| 2d68321503 | |||
| 85455ab21c | |||
| 18d63eb980 | |||
| 2e429e9704 | |||
| f4af28059b | |||
| c1476bd075 | |||
| 16c720fddd | |||
| 8b4b4a5eca | |||
| 32e6e5b7e2 | |||
| a9f7fd8e32 | |||
| 95be1a26f2 | |||
| ef9b31aee0 | |||
| df3c01ff0a | |||
| 4704c5a33d | |||
| 225c06550a | |||
| f0987614c6 | |||
| 269c2876f3 | |||
| 168d6acf7c | |||
| a5f718e257 | |||
| 4f68459582 | |||
| 442db122cf | |||
| 580d757be2 | |||
| 47b0f6d2d8 | |||
| 3f6f6ebc2b | |||
| 2645ea29d6 | |||
| 79f2041565 | |||
| 4a7567f288 | |||
| 8a890ed6ed | |||
| 3d90df6897 | |||
| 681c13144a | |||
| b64f2e6d32 | |||
| 428eabb1bd | |||
| 2162e83bce | |||
| 6142022283 | |||
| e6d9c8250f | |||
| 46178e723b | |||
| 605f55acd2 | |||
| 0f8b69aa60 | |||
| 551d8ec480 | |||
| fc48446ec4 | |||
| 3644b83ac6 | |||
| cd73791b6f | |||
| 03de71df2e | |||
| 83d5d3faf4 | |||
| 0c8bec4c89 | |||
| 7fc93817c1 | |||
| d0a00031a1 | |||
| 6ebc5aa0c2 | |||
| 8a6b4bb49e | |||
| 20799b9a3e | |||
| ec3afc615c | |||
| da692ccfc3 | |||
| 53b89f3a18 | |||
| 58cded28d3 | |||
| 592c5f3732 | |||
| f44a78a5f5 | |||
| e0bdd5dfdc | |||
| 99c0f06b72 | |||
| 66fc6df3d7 | |||
| a36425a905 | |||
| d920fee6d4 | |||
| 23590be5de | |||
| 94acc3c93b | |||
| 5616326a3b | |||
| 7601860c5c | |||
| 8b43d785cc | |||
| b62d3a0b7d | |||
| fad73c7213 | |||
| 2403c7c2b0 | |||
| fa090bf920 | |||
| a83ee86340 | |||
| 204955bcce | |||
| ee56e9543a | |||
| 96fdff6ffd | |||
| 58b007cbb3 | |||
| 4f0c139889 | |||
| c584c24fce | |||
| 85e5cee154 | |||
| 5af91981f1 | |||
| 2fe4f08059 | |||
| 83796f345a | |||
| c1e21364a6 | |||
| 067d9d0d3b | |||
| 03f527d83e | |||
| ced05a4586 | |||
| 43fe06206a | |||
| 023657558e | |||
| 9b0b726c80 | |||
| 4ee67321c4 | |||
| 59f1f2e59b | |||
| 0766d48b7c | |||
| e18903b9e9 | |||
| d0eecdead2 | |||
| cc4a83a033 | |||
| 1cf911bbde | |||
| 36d73d5023 | |||
| c395242e3e | |||
| cd9cd7cc5d | |||
| acbb8a0c07 | |||
| b9d8528599 | |||
| 4971326eca | |||
| 09d1047260 | |||
| 02dbd493c2 | |||
| b17931e7bd | |||
| 2a4570eafc | |||
| c9514d3a6d | |||
| 072805efc7 | |||
| 369ff26627 | |||
| c5abbbbd2d | |||
| d974639f1e | |||
| 26efde316b | |||
| fafe50f80a | |||
| 41504469db | |||
| 03b3ddd98b | |||
| 89d919f2be | |||
| b53cfd5504 | |||
| 31022733ac | |||
| f9a8c39879 | |||
| a812c2dd2f | |||
| 217e977f0d | |||
| 04c301d1ac | |||
| 7f0c425389 | |||
| 1ede1c423b | |||
| 7cb064896a | |||
| 0c5e2862ca | |||
| 30d4507f54 | |||
| b6c6d89455 | |||
| 9d218ee534 | |||
| 11116d8cab | |||
| 58d754bbde | |||
| 8f25c123dd | |||
| 76e214fc08 | |||
| 2b5380f8d6 | |||
| 844a129094 | |||
| a3090cca9d | |||
| b7b5159e9c | |||
| 0f8bc2c950 | |||
| 69f5c49f45 | |||
| 9b308e6fb8 | |||
| 3e3f91128b | |||
| 0d1aae0ef7 | |||
| 4d022f0480 | |||
| 153e20d00e | |||
| a9a8171dd6 | |||
| bf5c3b59b2 | |||
| 607c432bdb | |||
| ae5c010770 | |||
| d5e432437f | |||
| 9f99ebce01 | |||
| 64ee899b84 | |||
| e0e0c1658b | |||
| 2c586f667c | |||
| 64164ef6c1 | |||
| 22343c0731 | |||
| f4ec1a4c60 | |||
| c1c33cceb1 | |||
| a3e975b2ba | |||
| 06e705a687 | |||
| d56eb6c867 | |||
| 9cbca864e3 | |||
| abb4378694 | |||
| 0eb698d9a4 | |||
| 15ea9f2093 | |||
| d47aca0923 | |||
| 1ac50e9959 | |||
| 6adfbe3a96 | |||
| 59f36e62e9 | |||
| 54af116009 | |||
| 38fbec8e3b | |||
| babbfc55e4 | |||
| 2511e18d69 | |||
| 29658c70a0 | |||
| 96311ee43d | |||
| bd33b09052 | |||
| 8055003b47 | |||
| 1257492f85 | |||
| 1107b7f4ef | |||
| a1a1171240 | |||
| 46c02e5df1 | |||
| 2e9efc57de | |||
| acecadef17 | |||
| 19394b5e69 | |||
| de999e197f | |||
| 9d95687d3c | |||
| aa9dfb4ab8 | |||
| 9c5b44efb3 | |||
| ac587a67e6 | |||
| 59428140a8 | |||
| 60bdb59d71 | |||
| be52871de8 | |||
| b7934cf357 | |||
| dbfbeef90a | |||
| 00943c9cdf | |||
| 8745c6a16a | |||
| 433ba4b58f | |||
| d40376e524 | |||
| a2982f88f5 | |||
| 1642f7abd9 | |||
| a10d2184ff | |||
| 522435f096 | |||
| 79b30290c0 | |||
| f8b8626859 | |||
| b061b85a08 | |||
| 3870db1c88 | |||
| 1be1070eb4 | |||
| 2696e663cf | |||
| 1e1f7df86d | |||
| 1d8ded8fd3 | |||
| 197825123a | |||
| 422b2e6db1 | |||
| 1973e0b5bf | |||
| 8258cf93a9 | |||
| 1d49bd5947 | |||
| 44317d1519 | |||
| 48e08fcc69 | |||
| 1a3ce6e623 | |||
| fb122f3e70 | |||
| 7cca6039cc | |||
| 118e3dba39 | |||
| 87070710fa | |||
| 38499c3d4a | |||
| 5d754d968b | |||
| d543d436bc | |||
| 12b54f99af | |||
| 2a95d7e643 | |||
| e3d3cacfa4 | |||
| 4b13491a0e | |||
| 85d516d1e9 | |||
| ac58b6a7e3 | |||
| 2cc6126765 | |||
| f94b085850 | |||
| c9822a491b | |||
| 23b2d60295 | |||
| f4bc9eed39 | |||
| e310f188b0 | |||
| 6c1571188c | |||
| 945d2fa284 | |||
| 020095f1ff | |||
| b165a76e62 | |||
| 03c8830672 | |||
| 38448da89b | |||
| 2ade5aff91 | |||
| 29bf6e80ec | |||
| 027e927e1b | |||
| fa061f88e2 | |||
| 1d01b65b5f | |||
| 2c2b364167 | |||
| 7c5fc9bf7c | |||
| 193d22ff20 | |||
| 0f3b553ba2 | |||
| 4eb1013446 | |||
| be1b2563ec | |||
| 1c9c2f1e70 | |||
| 52dfded741 | |||
| 0a115e5cf4 | |||
| c349af999b | |||
| 9a5709a34d | |||
| 28d311160b | |||
| 1ca7081a40 | |||
| 5ce8ebe82c | |||
| a7a88b29b9 | |||
| cd7b982385 | |||
| ee59100075 | |||
| f808ac58ef | |||
| f33ada5396 | |||
| 984d781f2f | |||
| dd33e1e8bc | |||
| 2950de29e5 | |||
| 50a8799f9d | |||
| bdb0ce6fc7 | |||
| 18ec2bca96 | |||
| 937ef48794 | |||
| 6331f13e9a | |||
| 5213a2ff8e | |||
| 087d7452fd | |||
| 703094c924 | |||
| eea645c865 | |||
| 324832a189 | |||
| d55d735c51 | |||
| e3ff1b9609 | |||
| 70c31966ca | |||
| 0e4188882f | |||
| 6bf0ce92ba | |||
| d51bdc5086 | |||
| f04f262cee | |||
| 0f172055ef | |||
| e5dd4363f1 | |||
| a3a48bbaac | |||
| 5e716fb9a8 | |||
| 11a36153cc | |||
| 8bee354f04 | |||
| f7dd2b5ce7 | |||
| 8ca10c00bb | |||
| 905c7e8eda | |||
| d4c5e849bf | |||
| 8250a2a021 | |||
| 01b3df7b8c | |||
| daa6e4aff5 | |||
| 23bcb26a58 | |||
| e55f60c30b | |||
| 0d0c16e16d | |||
| 540d5cce7c | |||
| a548b00979 | |||
| 4b7ca6d565 | |||
| 0473fa238c | |||
| cfc7119697 | |||
| 22a6520d3e | |||
| fb25e91191 | |||
| c116b270b6 | |||
| aa2d598689 | |||
| 5ef3bb746b | |||
| 037616e271 | |||
| abbea906f1 | |||
| 9132e216c9 | |||
| 12a7e3c4af | |||
| b40c093917 | |||
| 7ac12455c8 | |||
| 5043eec7a2 | |||
| cf31f53e01 | |||
| cd22d76fa7 | |||
| c8759843f7 | |||
| 781bbcc012 | |||
| 6da29c0686 | |||
| fcdb6fc45a | |||
| e785a99bd7 | |||
| 121e513fdd | |||
| 9cf01ab54f | |||
| c655107681 | |||
| 91a5af6a9a | |||
| 86e74c0a6f | |||
| d8f01f21a0 | |||
| 9fb8626d8c | |||
| 1c52e04cdb | |||
| 798128256e | |||
| 72c2df47fd | |||
| c9c6d4c0c1 | |||
| 2f4f9f3003 | |||
| 22e8f8e5d6 | |||
| 04cf8c3d9a | |||
| 52198be543 | |||
| 80fd5a489b | |||
| b187043ee1 | |||
| 8965752055 | |||
| b796620267 | |||
| 62df81bb4e | |||
| 0a8e0f6178 | |||
| f705020aaa | |||
| 78bd3b9853 | |||
| 992b283597 | |||
| 8fc1ff1d59 | |||
| 3bbde61f39 | |||
| 8d955c4b9d | |||
| 18593c530b | |||
| 78903cd4eb | |||
| eaa143f7d7 | |||
| bcb0e42fa2 | |||
| 8eed435302 | |||
| 0e4a63057f | |||
| 1af5faa440 | |||
| 7412217b0c | |||
| 3e749d75b7 | |||
| 846b5c87c9 | |||
| 7b5e84f80b | |||
| 27822a8a66 | |||
| 36f4e7ec37 | |||
| 187e84ad65 | |||
| 195fe221c4 | |||
| 6f9c19bbf6 | |||
| 65bdab4f7e | |||
| c4d5fcfc22 | |||
| 43232afa62 | |||
| 18908a01d7 | |||
| b22fbfb3bc | |||
| 57aaea88b6 | |||
| 140949c5ea | |||
| 639241e0d3 | |||
| e0eb42bc2d | |||
| 7990b21cc5 | |||
| 2ba5c97709 | |||
| 33b7c85fc2 | |||
| 7f6c02ffdf | |||
| f368616e6f | |||
| dd632f4203 | |||
| a8f3ae501b | |||
| ea76963ac2 | |||
| 99dfa97958 | |||
| f68270a5b3 | |||
| 542ed81034 | |||
| 404a11f5e7 | |||
| 411221070e | |||
| 25d35d0c76 | |||
| c72904d61c | |||
| b94c9acd26 | |||
| bdb4988569 | |||
| 1dafd1352a | |||
| 31fbd3cad7 | |||
| 848bc5ec10 | |||
| 4d0ad826a0 | |||
| 6955820fcc | |||
| ef530624b9 | |||
| 9b9e7dd88f | |||
| a13cc9e961 | |||
| 0d2b923378 | |||
| fba84c8ac8 | |||
| db10fe1b2c | |||
| 175dd980f8 | |||
| 8364020671 | |||
| eba44cd394 | |||
| b3bac8015a | |||
| 0b48afd251 | |||
| 19857930a4 | |||
| d0dbe3ed2f | |||
| 8b7e78b63a | |||
| 92a4ecb523 | |||
| 6a5ad4d728 | |||
| be4aa8daac | |||
| b5eac37782 | |||
| b1ad3c5a39 | |||
| ba16bad029 | |||
| ca8ae4cd72 | |||
| 53d35d74b3 | |||
| 49c139e235 | |||
| caf9c821f3 | |||
| ca4c6db96f | |||
| 6b2298c752 | |||
| a1bf43def9 | |||
| 15e9254e00 | |||
| afe5a72c6f | |||
| 750a8b9ecf | |||
| 27fc3f93e0 | |||
| 8166d4b99b | |||
| b61d2ae2eb | |||
| 4790fe0aea | |||
| bc37b11cee | |||
| 223fed910f | |||
| b85ab7b061 | |||
| 888dc299c9 | |||
| e113dc28ed | |||
| 31e55d2307 | |||
| e90645f248 | |||
| 4bb7ba2571 | |||
| 8d31c25bf8 | |||
| c7ee1c28b2 | |||
| 99b09c8b28 | |||
| a328c4425a | |||
| c0f61ca896 | |||
| 86e70c0961 | |||
| d87a3a039f | |||
| 6279a2c40a | |||
| f377ebea88 | |||
| 7373fef964 | |||
| 41ef86dbda | |||
| 7bc2b2336d | |||
| 0a615e6d78 | |||
| 8f928e16e1 | |||
| 72bc8da1e7 | |||
| 51e349a5db | |||
| 3cbb19ba2c | |||
| ae3a5bf45d | |||
| 9c667f4b78 | |||
| 21195e1bcb | |||
| 03117ac565 | |||
| d13fbdf176 | |||
| 7cecc0e0b6 | |||
| 203e781f5d | |||
| 3eb6cd77cd | |||
| 51855b2405 | 
| @@ -1,17 +1,14 @@ | |||||||
| 
 | 
 | ||||||
| name: Publish package to GitHub Packages | name: Build | ||||||
| on: [push] | on: [push] | ||||||
| jobs: | jobs: | ||||||
|   publishing: |   build: | ||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|     steps: |     steps: | ||||||
|       - uses: actions/checkout@v2 |       - uses: actions/checkout@v2 | ||||||
|       - uses: actions/setup-java@v1 |       - uses: actions/setup-java@v1 | ||||||
|         with: |         with: | ||||||
|           java-version: 11 |           java-version: 17 | ||||||
|       - name: Fix android 32.0.0 dx |  | ||||||
|         continue-on-error: true |  | ||||||
|         run: cd /usr/local/lib/android/sdk/build-tools/32.0.0/ && mv d8 dx && cd lib  && mv d8.jar dx.jar |  | ||||||
|       - name: Rewrite version |       - name: Rewrite version | ||||||
|         run: | |         run: | | ||||||
|           branch="`echo "${{ github.ref }}" | grep -o "[^/]*$"`" |           branch="`echo "${{ github.ref }}" | grep -o "[^/]*$"`" | ||||||
| @@ -22,7 +19,6 @@ jobs: | |||||||
|         run: ./gradlew build |         run: ./gradlew build | ||||||
|       - name: Publish |       - name: Publish | ||||||
|         continue-on-error: true |         continue-on-error: true | ||||||
|         run: ./gradlew --no-parallel publishAllPublicationsToGithubPackagesRepository |         run: ./gradlew publishAllPublicationsToGiteaRepository | ||||||
|         env: |         env: | ||||||
|           GITHUBPACKAGES_USER: ${{ github.actor }} |           GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} | ||||||
|           GITHUBPACKAGES_PASSWORD: ${{ secrets.GITHUB_TOKEN }} |  | ||||||
							
								
								
									
										7
									
								
								.github/workflows/dokka_push.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										7
									
								
								.github/workflows/dokka_push.yml
									
									
									
									
										vendored
									
									
								
							| @@ -10,12 +10,9 @@ jobs: | |||||||
|       - uses: actions/checkout@v2 |       - uses: actions/checkout@v2 | ||||||
|       - uses: actions/setup-java@v1 |       - uses: actions/setup-java@v1 | ||||||
|         with: |         with: | ||||||
|           java-version: 11 |           java-version: 17 | ||||||
|       - name: Fix android 32.0.0 dx |  | ||||||
|         continue-on-error: true |  | ||||||
|         run: cd /usr/local/lib/android/sdk/build-tools/32.0.0/ && mv d8 dx && cd lib  && mv d8.jar dx.jar |  | ||||||
|       - name: Build |       - name: Build | ||||||
|         run: ./gradlew dokkaHtml |         run: ./gradlew build && ./gradlew dokkaHtml | ||||||
|       - name: Publish KDocs |       - name: Publish KDocs | ||||||
|         uses: peaceiris/actions-gh-pages@v3 |         uses: peaceiris/actions-gh-pages@v3 | ||||||
|         with: |         with: | ||||||
|   | |||||||
| @@ -1,8 +0,0 @@ | |||||||
| job("Build and run tests") { |  | ||||||
|     container(displayName = "Run gradle build", image = "openjdk:11") { |  | ||||||
|         kotlinScript { api -> |  | ||||||
|             // here can be your complex logic |  | ||||||
|             api.gradlew("build") |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
							
								
								
									
										995
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										995
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | <manifest/> | ||||||
| @@ -1 +0,0 @@ | |||||||
| <manifest package="dev.inmo.micro_utils.android.alerts.common"/> |  | ||||||
| @@ -0,0 +1 @@ | |||||||
|  | <manifest/> | ||||||
| @@ -1 +0,0 @@ | |||||||
| <manifest package="dev.inmo.micro_utils.android.alerts.recyclerview"/> |  | ||||||
							
								
								
									
										18
									
								
								android/pickers/build.gradle
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								android/pickers/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 { | ||||||
|  |         androidMain { | ||||||
|  |             dependencies { | ||||||
|  |                 api project(":micro_utils.android.smalltextfield") | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										1
									
								
								android/pickers/src/androidMain/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								android/pickers/src/androidMain/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | <manifest/> | ||||||
							
								
								
									
										27
									
								
								android/pickers/src/androidMain/kotlin/Fling.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								android/pickers/src/androidMain/kotlin/Fling.kt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | |||||||
|  | package dev.inmo.micro_utils.android.pickers | ||||||
|  |  | ||||||
|  | import androidx.compose.animation.core.* | ||||||
|  |  | ||||||
|  | internal suspend fun Animatable<Float, AnimationVector1D>.fling( | ||||||
|  |     initialVelocity: Float, | ||||||
|  |     animationSpec: DecayAnimationSpec<Float>, | ||||||
|  |     adjustTarget: ((Float) -> Float)?, | ||||||
|  |     block: (Animatable<Float, AnimationVector1D>.() -> Unit)? = null, | ||||||
|  | ): AnimationResult<Float, AnimationVector1D> { | ||||||
|  |     val targetValue = animationSpec.calculateTargetValue(value, initialVelocity) | ||||||
|  |     val adjustedTarget = adjustTarget?.invoke(targetValue) | ||||||
|  |  | ||||||
|  |     return if (adjustedTarget != null) { | ||||||
|  |         animateTo( | ||||||
|  |             targetValue = adjustedTarget, | ||||||
|  |             initialVelocity = initialVelocity, | ||||||
|  |             block = block | ||||||
|  |         ) | ||||||
|  |     } else { | ||||||
|  |         animateDecay( | ||||||
|  |             initialVelocity = initialVelocity, | ||||||
|  |             animationSpec = animationSpec, | ||||||
|  |             block = block, | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										222
									
								
								android/pickers/src/androidMain/kotlin/NumberPicker.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										222
									
								
								android/pickers/src/androidMain/kotlin/NumberPicker.kt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,222 @@ | |||||||
|  | package dev.inmo.micro_utils.android.pickers | ||||||
|  |  | ||||||
|  | import androidx.compose.animation.core.Animatable | ||||||
|  | import androidx.compose.animation.core.exponentialDecay | ||||||
|  | import androidx.compose.foundation.clickable | ||||||
|  | import androidx.compose.foundation.gestures.* | ||||||
|  | import androidx.compose.foundation.layout.* | ||||||
|  | import androidx.compose.foundation.text.KeyboardActions | ||||||
|  | import androidx.compose.foundation.text.KeyboardOptions | ||||||
|  | import androidx.compose.material.ContentAlpha | ||||||
|  | import androidx.compose.material.IconButton | ||||||
|  | import androidx.compose.material.ProvideTextStyle | ||||||
|  | import androidx.compose.material.icons.Icons | ||||||
|  | import androidx.compose.material.icons.filled.KeyboardArrowDown | ||||||
|  | import androidx.compose.material.icons.filled.KeyboardArrowUp | ||||||
|  | import androidx.compose.material3.* | ||||||
|  | import androidx.compose.runtime.* | ||||||
|  | import androidx.compose.ui.Alignment | ||||||
|  | import androidx.compose.ui.ExperimentalComposeUiApi | ||||||
|  | import androidx.compose.ui.Modifier | ||||||
|  | import androidx.compose.ui.draw.alpha | ||||||
|  | import androidx.compose.ui.focus.FocusRequester | ||||||
|  | import androidx.compose.ui.focus.focusRequester | ||||||
|  | import androidx.compose.ui.geometry.Offset | ||||||
|  | import androidx.compose.ui.graphics.Color | ||||||
|  | import androidx.compose.ui.input.pointer.PointerInputScope | ||||||
|  | import androidx.compose.ui.input.pointer.pointerInput | ||||||
|  | import androidx.compose.ui.platform.LocalDensity | ||||||
|  | import androidx.compose.ui.text.ExperimentalTextApi | ||||||
|  | import androidx.compose.ui.text.TextStyle | ||||||
|  | import androidx.compose.ui.text.input.KeyboardType | ||||||
|  | import androidx.compose.ui.unit.IntOffset | ||||||
|  | import androidx.compose.ui.unit.center | ||||||
|  | import androidx.compose.ui.unit.dp | ||||||
|  | import dev.inmo.micro_utils.android.smalltextfield.SmallTextField | ||||||
|  | import kotlinx.coroutines.launch | ||||||
|  | import kotlin.math.abs | ||||||
|  | import kotlin.math.absoluteValue | ||||||
|  | import kotlin.math.roundToInt | ||||||
|  |  | ||||||
|  | private inline fun PointerInputScope.checkContains(offset: Offset): Boolean { | ||||||
|  |     return ((size.center.x - offset.x).absoluteValue < size.width / 2) && ((size.center.y - offset.y).absoluteValue < size.height / 2) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // src: https://gist.github.com/vganin/a9a84653a9f48a2d669910fbd48e32d5 | ||||||
|  |  | ||||||
|  | @OptIn(ExperimentalTextApi::class, ExperimentalComposeUiApi::class) | ||||||
|  | @Composable | ||||||
|  | fun NumberPicker( | ||||||
|  |     number: Int, | ||||||
|  |     modifier: Modifier = Modifier, | ||||||
|  |     range: IntRange? = null, | ||||||
|  |     textStyle: TextStyle = LocalTextStyle.current, | ||||||
|  |     arrowsColor: Color = MaterialTheme.colorScheme.primary, | ||||||
|  |     allowUseManualInput: Boolean = true, | ||||||
|  |     onStateChanged: (Int) -> Unit = {}, | ||||||
|  | ) { | ||||||
|  |     val coroutineScope = rememberCoroutineScope() | ||||||
|  |     val numbersColumnHeight = 36.dp | ||||||
|  |     val halvedNumbersColumnHeight = numbersColumnHeight / 2 | ||||||
|  |     val halvedNumbersColumnHeightPx = with(LocalDensity.current) { halvedNumbersColumnHeight.toPx() } | ||||||
|  |  | ||||||
|  |     fun animatedStateValue(offset: Float): Int = number - (offset / halvedNumbersColumnHeightPx).toInt() | ||||||
|  |  | ||||||
|  |     val animatedOffset = remember { Animatable(0f) }.apply { | ||||||
|  |         if (range != null) { | ||||||
|  |             val offsetRange = remember(number, range) { | ||||||
|  |                 val value = number | ||||||
|  |                 val first = -(range.last - value) * halvedNumbersColumnHeightPx | ||||||
|  |                 val last = -(range.first - value) * halvedNumbersColumnHeightPx | ||||||
|  |                 first..last | ||||||
|  |             } | ||||||
|  |             updateBounds(offsetRange.start, offsetRange.endInclusive) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     val coercedAnimatedOffset = animatedOffset.value % halvedNumbersColumnHeightPx | ||||||
|  |     val animatedStateValue = animatedStateValue(animatedOffset.value) | ||||||
|  |     val disabledArrowsColor = arrowsColor.copy(alpha = ContentAlpha.disabled) | ||||||
|  |  | ||||||
|  |     val inputFieldShown = if (allowUseManualInput) { | ||||||
|  |         remember { mutableStateOf(false) } | ||||||
|  |     } else { | ||||||
|  |         null | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     Column( | ||||||
|  |         modifier = modifier | ||||||
|  |             .wrapContentSize() | ||||||
|  |             .draggable( | ||||||
|  |                 orientation = Orientation.Vertical, | ||||||
|  |                 state = rememberDraggableState { deltaY -> | ||||||
|  |                     if (inputFieldShown ?.value != true) { | ||||||
|  |                         coroutineScope.launch { | ||||||
|  |                             animatedOffset.snapTo(animatedOffset.value + deltaY) | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 }, | ||||||
|  |                 onDragStopped = { velocity -> | ||||||
|  |                     if (inputFieldShown ?.value != true) { | ||||||
|  |                         coroutineScope.launch { | ||||||
|  |                             val endValue = animatedOffset.fling( | ||||||
|  |                                 initialVelocity = velocity, | ||||||
|  |                                 animationSpec = exponentialDecay(frictionMultiplier = 20f), | ||||||
|  |                                 adjustTarget = { target -> | ||||||
|  |                                     val coercedTarget = target % halvedNumbersColumnHeightPx | ||||||
|  |                                     val coercedAnchors = | ||||||
|  |                                         listOf(-halvedNumbersColumnHeightPx, 0f, halvedNumbersColumnHeightPx) | ||||||
|  |                                     val coercedPoint = coercedAnchors.minByOrNull { abs(it - coercedTarget) }!! | ||||||
|  |                                     val base = | ||||||
|  |                                         halvedNumbersColumnHeightPx * (target / halvedNumbersColumnHeightPx).toInt() | ||||||
|  |                                     coercedPoint + base | ||||||
|  |                                 } | ||||||
|  |                             ).endState.value | ||||||
|  |  | ||||||
|  |                             onStateChanged(animatedStateValue(endValue)) | ||||||
|  |                             animatedOffset.snapTo(0f) | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             ), | ||||||
|  |         horizontalAlignment = Alignment.CenterHorizontally | ||||||
|  |     ) { | ||||||
|  |         val spacing = 4.dp | ||||||
|  |  | ||||||
|  |         val upEnabled = range == null || range.first < number | ||||||
|  |         IconButton( | ||||||
|  |             { | ||||||
|  |                 onStateChanged(number - 1) | ||||||
|  |                 inputFieldShown ?.value = false | ||||||
|  |             }, | ||||||
|  |             enabled = upEnabled | ||||||
|  |         ) { | ||||||
|  |             Icon(Icons.Default.KeyboardArrowUp, "", tint = if (upEnabled) arrowsColor else disabledArrowsColor) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         Spacer(modifier = Modifier.height(spacing)) | ||||||
|  |         Box( | ||||||
|  |             modifier = Modifier | ||||||
|  |                 .offset { IntOffset(x = 0, y = coercedAnimatedOffset.roundToInt()) }, | ||||||
|  |             contentAlignment = Alignment.Center | ||||||
|  |         ) { | ||||||
|  |             val baseLabelModifier = Modifier.align(Alignment.Center) | ||||||
|  |             ProvideTextStyle(textStyle) { | ||||||
|  |                 Text( | ||||||
|  |                     text = (animatedStateValue - 1).toString(), | ||||||
|  |                     modifier = baseLabelModifier | ||||||
|  |                         .offset(y = -halvedNumbersColumnHeight) | ||||||
|  |                         .alpha(coercedAnimatedOffset / halvedNumbersColumnHeightPx) | ||||||
|  |                 ) | ||||||
|  |  | ||||||
|  |                 if (inputFieldShown ?.value == true) { | ||||||
|  |                     val currentValue = remember { mutableStateOf(number.toString()) } | ||||||
|  |  | ||||||
|  |                     val focusRequester = remember { FocusRequester() } | ||||||
|  |                     SmallTextField( | ||||||
|  |                         currentValue.value, | ||||||
|  |                         { | ||||||
|  |                             val asDigit = it.toIntOrNull() | ||||||
|  |                             when { | ||||||
|  |                                 (asDigit == null && it.isEmpty()) -> currentValue.value = (range ?.first ?: 0).toString() | ||||||
|  |                                 (asDigit != null && (range == null || asDigit in range)) -> currentValue.value = it | ||||||
|  |                                 else -> { /* do nothing */ } | ||||||
|  |                             } | ||||||
|  |                         }, | ||||||
|  |                         baseLabelModifier.focusRequester(focusRequester).width(IntrinsicSize.Min).pointerInput(number) { | ||||||
|  |                             detectTapGestures { | ||||||
|  |                                 if (!checkContains(it)) { | ||||||
|  |                                     currentValue.value.toIntOrNull() ?.let(onStateChanged) | ||||||
|  |                                     inputFieldShown.value = false | ||||||
|  |                                 } | ||||||
|  |                             } | ||||||
|  |                         }, | ||||||
|  |                         keyboardOptions = KeyboardOptions( | ||||||
|  |                             keyboardType = KeyboardType.Number | ||||||
|  |                         ), | ||||||
|  |                         keyboardActions = KeyboardActions { | ||||||
|  |                             currentValue.value.toIntOrNull() ?.let(onStateChanged) | ||||||
|  |                             inputFieldShown.value = false | ||||||
|  |                         }, | ||||||
|  |                         singleLine = true, | ||||||
|  |                         textStyle = textStyle | ||||||
|  |                     ) | ||||||
|  |                     LaunchedEffect(Unit) { | ||||||
|  |                         focusRequester.requestFocus() | ||||||
|  |                     } | ||||||
|  |                 } else { | ||||||
|  |                     Text( | ||||||
|  |                         text = animatedStateValue.toString(), | ||||||
|  |                         modifier = baseLabelModifier | ||||||
|  |                             .alpha(1 - abs(coercedAnimatedOffset) / halvedNumbersColumnHeightPx) | ||||||
|  |                             .clickable { | ||||||
|  |                                 if (inputFieldShown ?.value == false) { | ||||||
|  |                                     inputFieldShown.value = true | ||||||
|  |                                 } | ||||||
|  |                             } | ||||||
|  |                     ) | ||||||
|  |                 } | ||||||
|  |                 Text( | ||||||
|  |                     text = (animatedStateValue + 1).toString(), | ||||||
|  |                     modifier = baseLabelModifier | ||||||
|  |                         .offset(y = halvedNumbersColumnHeight) | ||||||
|  |                         .alpha(-coercedAnimatedOffset / halvedNumbersColumnHeightPx) | ||||||
|  |                 ) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |         Spacer(modifier = Modifier.height(spacing)) | ||||||
|  |  | ||||||
|  |         val downEnabled = range == null || range.last > number | ||||||
|  |         IconButton( | ||||||
|  |             { | ||||||
|  |                 onStateChanged(number + 1) | ||||||
|  |                 inputFieldShown ?.value = false | ||||||
|  |             }, | ||||||
|  |             enabled = downEnabled | ||||||
|  |         ) { | ||||||
|  |             Icon(Icons.Default.KeyboardArrowDown, "", tint = if (downEnabled) arrowsColor else disabledArrowsColor) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
							
								
								
									
										156
									
								
								android/pickers/src/androidMain/kotlin/SetPicker.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										156
									
								
								android/pickers/src/androidMain/kotlin/SetPicker.kt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,156 @@ | |||||||
|  | package dev.inmo.micro_utils.android.pickers | ||||||
|  |  | ||||||
|  | import androidx.compose.animation.core.Animatable | ||||||
|  | import androidx.compose.animation.core.exponentialDecay | ||||||
|  | import androidx.compose.foundation.gestures.* | ||||||
|  | import androidx.compose.foundation.layout.* | ||||||
|  | import androidx.compose.foundation.rememberScrollState | ||||||
|  | import androidx.compose.material.ContentAlpha | ||||||
|  | import androidx.compose.material.icons.Icons | ||||||
|  | import androidx.compose.material.icons.filled.KeyboardArrowDown | ||||||
|  | import androidx.compose.material.icons.filled.KeyboardArrowUp | ||||||
|  | import androidx.compose.material3.* | ||||||
|  | import androidx.compose.runtime.* | ||||||
|  | import androidx.compose.ui.Alignment | ||||||
|  | import androidx.compose.ui.ExperimentalComposeUiApi | ||||||
|  | import androidx.compose.ui.Modifier | ||||||
|  | import androidx.compose.ui.draw.alpha | ||||||
|  | import androidx.compose.ui.graphics.Color | ||||||
|  | import androidx.compose.ui.platform.LocalDensity | ||||||
|  | import androidx.compose.ui.text.ExperimentalTextApi | ||||||
|  | import androidx.compose.ui.text.TextStyle | ||||||
|  | import androidx.compose.ui.unit.dp | ||||||
|  | import kotlinx.coroutines.launch | ||||||
|  | import kotlin.math.* | ||||||
|  |  | ||||||
|  | @OptIn(ExperimentalTextApi::class, ExperimentalComposeUiApi::class) | ||||||
|  | @Composable | ||||||
|  | fun <T> SetPicker( | ||||||
|  |     current: T, | ||||||
|  |     dataList: List<T>, | ||||||
|  |     modifier: Modifier = Modifier, | ||||||
|  |     textStyle: TextStyle = LocalTextStyle.current, | ||||||
|  |     arrowsColor: Color = MaterialTheme.colorScheme.primary, | ||||||
|  |     dataToString: @Composable (T) -> String = { it.toString() }, | ||||||
|  |     onStateChanged: (T) -> Unit = {}, | ||||||
|  | ) { | ||||||
|  |     val coroutineScope = rememberCoroutineScope() | ||||||
|  |     val numbersColumnHeight = 8.dp + with(LocalDensity.current) { | ||||||
|  |         textStyle.lineHeight.toDp() | ||||||
|  |     } | ||||||
|  |     val numbersColumnHeightPx = with(LocalDensity.current) { numbersColumnHeight.toPx() } | ||||||
|  |     val halvedNumbersColumnHeight = numbersColumnHeight / 2 | ||||||
|  |     val halvedNumbersColumnHeightPx = with(LocalDensity.current) { halvedNumbersColumnHeight.toPx() } | ||||||
|  |  | ||||||
|  |     val index = dataList.indexOfFirst { it === current }.takeIf { it > -1 } ?: dataList.indexOf(current) | ||||||
|  |     val lastIndex = dataList.size - 1 | ||||||
|  |  | ||||||
|  |     fun animatedStateValue(offset: Float): Int = index - (offset / halvedNumbersColumnHeightPx).toInt() | ||||||
|  |  | ||||||
|  |     val animatedOffset = remember { Animatable(0f) }.apply { | ||||||
|  |         val offsetRange = remember(index, lastIndex) { | ||||||
|  |             val value = index | ||||||
|  |             val first = -(lastIndex - value) * halvedNumbersColumnHeightPx | ||||||
|  |             val last = value * halvedNumbersColumnHeightPx | ||||||
|  |             first..last | ||||||
|  |         } | ||||||
|  |         updateBounds(offsetRange.start, offsetRange.endInclusive) | ||||||
|  |     } | ||||||
|  |     val indexAnimatedOffset = if (animatedOffset.value > 0) { | ||||||
|  |         (index - floor(animatedOffset.value / halvedNumbersColumnHeightPx).toInt()) | ||||||
|  |     } else { | ||||||
|  |         (index - ceil(animatedOffset.value / halvedNumbersColumnHeightPx).toInt()) | ||||||
|  |     } | ||||||
|  |     val coercedAnimatedOffset = animatedOffset.value % halvedNumbersColumnHeightPx | ||||||
|  |     val boxOffset = (indexAnimatedOffset * halvedNumbersColumnHeightPx) - coercedAnimatedOffset | ||||||
|  |     val disabledArrowsColor = arrowsColor.copy(alpha = ContentAlpha.disabled) | ||||||
|  |     val scrollState = rememberScrollState() | ||||||
|  |  | ||||||
|  |     Column( | ||||||
|  |         modifier = modifier | ||||||
|  |             .wrapContentSize() | ||||||
|  |             .draggable( | ||||||
|  |                 orientation = Orientation.Vertical, | ||||||
|  |                 state = rememberDraggableState { deltaY -> | ||||||
|  |                     coroutineScope.launch { | ||||||
|  |                         animatedOffset.snapTo(animatedOffset.value + deltaY) | ||||||
|  |                     } | ||||||
|  |                 }, | ||||||
|  |                 onDragStopped = { velocity -> | ||||||
|  |                     coroutineScope.launch { | ||||||
|  |                         val endValue = animatedOffset.fling( | ||||||
|  |                             initialVelocity = velocity, | ||||||
|  |                             animationSpec = exponentialDecay(frictionMultiplier = 20f), | ||||||
|  |                             adjustTarget = { target -> | ||||||
|  |                                 val coercedTarget = target % halvedNumbersColumnHeightPx | ||||||
|  |                                 val coercedAnchors = | ||||||
|  |                                     listOf(-halvedNumbersColumnHeightPx, 0f, halvedNumbersColumnHeightPx) | ||||||
|  |                                 val coercedPoint = coercedAnchors.minByOrNull { abs(it - coercedTarget) }!! | ||||||
|  |                                 val base = | ||||||
|  |                                     halvedNumbersColumnHeightPx * (target / halvedNumbersColumnHeightPx).toInt() | ||||||
|  |                                 coercedPoint + base | ||||||
|  |                             } | ||||||
|  |                         ).endState.value | ||||||
|  |  | ||||||
|  |                         onStateChanged(dataList.elementAt(animatedStateValue(endValue))) | ||||||
|  |                         animatedOffset.snapTo(0f) | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             ), | ||||||
|  |         horizontalAlignment = Alignment.CenterHorizontally | ||||||
|  |     ) { | ||||||
|  |         val spacing = 4.dp | ||||||
|  |  | ||||||
|  |         val upEnabled = index > 0 | ||||||
|  |         IconButton( | ||||||
|  |             { | ||||||
|  |                 onStateChanged(dataList.elementAt(index - 1)) | ||||||
|  |             }, | ||||||
|  |             enabled = upEnabled | ||||||
|  |         ) { | ||||||
|  |             Icon(Icons.Default.KeyboardArrowUp, "", tint = if (upEnabled) arrowsColor else disabledArrowsColor) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         Spacer(modifier = Modifier.height(spacing)) | ||||||
|  |         Box( | ||||||
|  |             modifier = Modifier, | ||||||
|  |             contentAlignment = Alignment.Center | ||||||
|  |         ) { | ||||||
|  |             ProvideTextStyle(textStyle) { | ||||||
|  |                 dataList.forEachIndexed { i, t -> | ||||||
|  |                     val alpha = when { | ||||||
|  |                         i == indexAnimatedOffset - 1 -> coercedAnimatedOffset / halvedNumbersColumnHeightPx | ||||||
|  |                         i == indexAnimatedOffset -> 1 - (abs(coercedAnimatedOffset) / halvedNumbersColumnHeightPx) | ||||||
|  |                         i == indexAnimatedOffset + 1 -> -coercedAnimatedOffset / halvedNumbersColumnHeightPx | ||||||
|  |                         else -> return@forEachIndexed | ||||||
|  |                     } | ||||||
|  |                     val offset = when { | ||||||
|  |                         i == indexAnimatedOffset - 1 && coercedAnimatedOffset > 0 -> coercedAnimatedOffset - halvedNumbersColumnHeightPx | ||||||
|  |                         i == indexAnimatedOffset -> coercedAnimatedOffset | ||||||
|  |                         i == indexAnimatedOffset + 1 && coercedAnimatedOffset < 0 -> coercedAnimatedOffset + halvedNumbersColumnHeightPx | ||||||
|  |                         else -> return@forEachIndexed | ||||||
|  |                     } | ||||||
|  |                     Text( | ||||||
|  |                         text = dataToString(t), | ||||||
|  |                         modifier = Modifier | ||||||
|  |                             .alpha(alpha) | ||||||
|  |                             .offset(y = with(LocalDensity.current) { offset.toDp() }) | ||||||
|  |                     ) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |         Spacer(modifier = Modifier.height(spacing)) | ||||||
|  |  | ||||||
|  |         val downEnabled = index < lastIndex | ||||||
|  |         IconButton( | ||||||
|  |             { | ||||||
|  |                 onStateChanged(dataList.elementAt(index + 1)) | ||||||
|  |             }, | ||||||
|  |             enabled = downEnabled | ||||||
|  |         ) { | ||||||
|  |             Icon(Icons.Default.KeyboardArrowDown, "", tint = if (downEnabled) arrowsColor else disabledArrowsColor) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										1
									
								
								android/recyclerview/src/androidMain/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								android/recyclerview/src/androidMain/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | <manifest/> | ||||||
| @@ -1 +0,0 @@ | |||||||
| <manifest package="dev.inmo.micro_utils.android.recyclerview"/> |  | ||||||
							
								
								
									
										18
									
								
								android/smalltextfield/build.gradle
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								android/smalltextfield/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 { | ||||||
|  |         androidMain { | ||||||
|  |             dependencies { | ||||||
|  |                 api libs.android.compose.material3 | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1 @@ | |||||||
|  | <manifest/> | ||||||
| @@ -0,0 +1,66 @@ | |||||||
|  | package dev.inmo.micro_utils.android.smalltextfield | ||||||
|  |  | ||||||
|  | import androidx.compose.foundation.interaction.MutableInteractionSource | ||||||
|  | import androidx.compose.foundation.layout.Box | ||||||
|  | import androidx.compose.foundation.layout.Row | ||||||
|  | import androidx.compose.foundation.layout.defaultMinSize | ||||||
|  | import androidx.compose.foundation.text.BasicTextField | ||||||
|  | import androidx.compose.foundation.text.KeyboardActions | ||||||
|  | import androidx.compose.foundation.text.KeyboardOptions | ||||||
|  | import androidx.compose.foundation.text.selection.LocalTextSelectionColors | ||||||
|  | import androidx.compose.material3.* | ||||||
|  | import androidx.compose.runtime.Composable | ||||||
|  | import androidx.compose.runtime.CompositionLocalProvider | ||||||
|  | import androidx.compose.runtime.remember | ||||||
|  | import androidx.compose.ui.Alignment | ||||||
|  | import androidx.compose.ui.Modifier | ||||||
|  | import androidx.compose.ui.graphics.Color | ||||||
|  | import androidx.compose.ui.graphics.Shape | ||||||
|  | import androidx.compose.ui.graphics.SolidColor | ||||||
|  | import androidx.compose.ui.graphics.takeOrElse | ||||||
|  | import androidx.compose.ui.text.TextStyle | ||||||
|  | import androidx.compose.ui.text.input.VisualTransformation | ||||||
|  |  | ||||||
|  | @OptIn(ExperimentalMaterial3Api::class) | ||||||
|  | @Composable | ||||||
|  | fun SmallTextField( | ||||||
|  |     value: String, | ||||||
|  |     onValueChange: (String) -> Unit, | ||||||
|  |     modifier: Modifier = Modifier, | ||||||
|  |     enabled: Boolean = true, | ||||||
|  |     readOnly: Boolean = false, | ||||||
|  |     textStyle: TextStyle = LocalTextStyle.current, | ||||||
|  |     textColor: Color = textStyle.color.takeOrElse { | ||||||
|  |         LocalContentColor.current | ||||||
|  |     }, | ||||||
|  |     visualTransformation: VisualTransformation = VisualTransformation.None, | ||||||
|  |     keyboardOptions: KeyboardOptions = KeyboardOptions.Default, | ||||||
|  |     keyboardActions: KeyboardActions = KeyboardActions.Default, | ||||||
|  |     singleLine: Boolean = false, | ||||||
|  |     maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE, | ||||||
|  |     minLines: Int = 1, | ||||||
|  |     interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, | ||||||
|  | ) { | ||||||
|  |     BasicTextField( | ||||||
|  |         value = value, | ||||||
|  |         modifier = modifier, | ||||||
|  |         onValueChange = onValueChange, | ||||||
|  |         enabled = enabled, | ||||||
|  |         readOnly = readOnly, | ||||||
|  |         textStyle = textStyle.copy( | ||||||
|  |             color = textColor | ||||||
|  |         ), | ||||||
|  |         visualTransformation = visualTransformation, | ||||||
|  |         keyboardOptions = keyboardOptions, | ||||||
|  |         keyboardActions = keyboardActions, | ||||||
|  |         interactionSource = interactionSource, | ||||||
|  |         singleLine = singleLine, | ||||||
|  |         maxLines = maxLines, | ||||||
|  |         minLines = minLines, | ||||||
|  |         cursorBrush = SolidColor( | ||||||
|  |             textStyle.color.takeOrElse { | ||||||
|  |                 LocalContentColor.current | ||||||
|  |             } | ||||||
|  |         ) | ||||||
|  |     ) | ||||||
|  | } | ||||||
| @@ -9,6 +9,7 @@ buildscript { | |||||||
|     dependencies { |     dependencies { | ||||||
|         classpath libs.buildscript.kt.gradle |         classpath libs.buildscript.kt.gradle | ||||||
|         classpath libs.buildscript.kt.serialization |         classpath libs.buildscript.kt.serialization | ||||||
|  |         classpath libs.buildscript.kt.ksp | ||||||
|         classpath libs.buildscript.jb.dokka |         classpath libs.buildscript.jb.dokka | ||||||
|         classpath libs.buildscript.gh.release |         classpath libs.buildscript.gh.release | ||||||
|         classpath libs.buildscript.android.gradle |         classpath libs.buildscript.android.gradle | ||||||
| @@ -16,11 +17,17 @@ buildscript { | |||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | plugins { | ||||||
|  |     alias(libs.plugins.versions) | ||||||
|  | } | ||||||
|  |  | ||||||
| allprojects { | allprojects { | ||||||
|     repositories { |     repositories { | ||||||
|         mavenLocal() |         mavenLocal() | ||||||
|         mavenCentral() |         mavenCentral() | ||||||
|         google() |         google() | ||||||
|  |         maven { url "https://maven.pkg.jetbrains.space/public/p/compose/dev" } | ||||||
|  |         maven { url "https://nexus.inmo.dev/repository/maven-releases/" } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     // temporal crutch until legacy tests will be stabled or legacy target will be removed |     // temporal crutch until legacy tests will be stabled or legacy target will be removed | ||||||
| @@ -35,3 +42,4 @@ allprojects { | |||||||
|  |  | ||||||
| apply from: "./extensions.gradle" | apply from: "./extensions.gradle" | ||||||
| apply from: "./github_release.gradle" | apply from: "./github_release.gradle" | ||||||
|  | apply from: "./versions_plugin_setup.gradle" | ||||||
|   | |||||||
| @@ -4,7 +4,7 @@ plugins { | |||||||
|     id "com.android.library" |     id "com.android.library" | ||||||
| } | } | ||||||
|  |  | ||||||
| apply from: "$mppProjectWithSerializationPresetPath" | apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64ProjectPresetPath" | ||||||
|  |  | ||||||
| kotlin { | kotlin { | ||||||
|     sourceSets { |     sourceSets { | ||||||
| @@ -16,6 +16,24 @@ kotlin { | |||||||
|         androidMain { |         androidMain { | ||||||
|             dependencies { |             dependencies { | ||||||
|                 api project(":micro_utils.coroutines") |                 api project(":micro_utils.coroutines") | ||||||
|  |                 api libs.android.fragment | ||||||
|  |             } | ||||||
|  |             dependsOn jvmMain | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         linuxX64Main { | ||||||
|  |             dependencies { | ||||||
|  |                 api libs.okio | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         mingwX64Main { | ||||||
|  |             dependencies { | ||||||
|  |                 api libs.okio | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         linuxArm64Main { | ||||||
|  |             dependencies { | ||||||
|  |                 api libs.okio | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|   | |||||||
							
								
								
									
										1
									
								
								common/compose/src/androidMain/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								common/compose/src/androidMain/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | <manifest/> | ||||||
| @@ -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,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 +0,0 @@ | |||||||
| <manifest package="dev.inmo.micro_utils.common.compose"/> |  | ||||||
							
								
								
									
										1
									
								
								common/src/androidMain/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								common/src/androidMain/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | <manifest/> | ||||||
| @@ -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 | ||||||
| @@ -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( | ||||||
|   | |||||||
| @@ -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,16 +16,33 @@ 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 | ||||||
|  */ |  */ | ||||||
| data class Diff<T> internal constructor( | @Serializable | ||||||
|     val removed: List<IndexedValue<T>>, | data class Diff<T> @Warning(warning) constructor( | ||||||
|  |     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() | ||||||
|  |  | ||||||
|  |     companion object { | ||||||
|  |         private const val warning = "This feature can be changed without any warranties. Use with caution and only in case you know what you are doing" | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fun <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>?>>, | ||||||
| @@ -32,17 +51,18 @@ private inline fun <T> performChanges( | |||||||
|     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 | ||||||
|                 } |                 } | ||||||
|             ) |             ) | ||||||
| @@ -92,7 +112,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,31 +140,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()) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 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( | inline fun <T> Iterable<T>.diff( | ||||||
|     other: Iterable<T>, |     other: Iterable<T>, | ||||||
|     strictComparison: Boolean = false |     strictComparison: Boolean = false | ||||||
| ): Diff<T> = calculateDiff(other, strictComparison) | ): 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) | 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) | ||||||
|  |  | ||||||
| /** | /** | ||||||
| @@ -154,6 +203,23 @@ inline fun <T> Iterable<T>.calculateStrictDiff( | |||||||
|     other: Iterable<T> |     other: Iterable<T> | ||||||
| ) = calculateDiff(other, strictComparison = true) | ) = calculateDiff(other, strictComparison = true) | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Applies [diff] to [this] [MutableList] | ||||||
|  |  */ | ||||||
|  | fun <T> MutableList<T>.applyDiff( | ||||||
|  |     diff: Diff<T> | ||||||
|  | ) { | ||||||
|  |     for (i in diff.removed.indices.sortedDescending()) { | ||||||
|  |         removeAt(diff.removed[i].index) | ||||||
|  |     } | ||||||
|  |     diff.added.forEach { (i, t) -> | ||||||
|  |         add(i, t) | ||||||
|  |     } | ||||||
|  |     diff.replaced.forEach { (_, new) -> | ||||||
|  |         set(new.index, new.value) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * This method call [calculateDiff] with strict mode [strictComparison] and then apply differences to [this] |  * This method call [calculateDiff] with strict mode [strictComparison] and then apply differences to [this] | ||||||
|  * mutable list |  * mutable list | ||||||
| @@ -161,14 +227,27 @@ inline fun <T> Iterable<T>.calculateStrictDiff( | |||||||
| fun <T> MutableList<T>.applyDiff( | fun <T> MutableList<T>.applyDiff( | ||||||
|     source: Iterable<T>, |     source: Iterable<T>, | ||||||
|     strictComparison: Boolean = false |     strictComparison: Boolean = false | ||||||
| ) = calculateDiff(source, strictComparison).let { | ): Diff<T> = calculateDiff(source, strictComparison).also { | ||||||
|     for (i in it.removed.indices.sortedDescending()) { |     applyDiff(it) | ||||||
|         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] and then apply differences to [this] | ||||||
|  |  * mutable list | ||||||
|  |  */ | ||||||
|  | fun <T> MutableList<T>.applyDiff( | ||||||
|  |     source: Iterable<T>, | ||||||
|  |     comparisonFun: (T?, T?) -> Boolean | ||||||
|  | ): Diff<T> = calculateDiff(source, comparisonFun).also { | ||||||
|  |     applyDiff(it) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Reverse [this] [Diff]. Result will contain [Diff.added] on [Diff.removed] (and vice-verse), all the | ||||||
|  |  * [Diff.replaced] values will be reversed too | ||||||
|  |  */ | ||||||
|  | fun <T> Diff<T>.reversed() = Diff( | ||||||
|  |     removed = added, | ||||||
|  |     replaced = replaced.map { it.second to it.first }, | ||||||
|  |     added = removed | ||||||
|  | ) | ||||||
|   | |||||||
| @@ -1,3 +1,5 @@ | |||||||
|  | @file:Suppress("unused", "NOTHING_TO_INLINE") | ||||||
|  |  | ||||||
| package dev.inmo.micro_utils.common | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
| import kotlinx.serialization.* | import kotlinx.serialization.* | ||||||
| @@ -21,26 +23,18 @@ import kotlinx.serialization.encoding.* | |||||||
| sealed interface Either<T1, T2> { | sealed interface Either<T1, T2> { | ||||||
|     val optionalT1: Optional<T1> |     val optionalT1: Optional<T1> | ||||||
|     val optionalT2: Optional<T2> |     val optionalT2: Optional<T2> | ||||||
|     @Deprecated("Use optionalT1 instead", ReplaceWith("optionalT1")) |  | ||||||
|     val t1: T1? |  | ||||||
|         get() = optionalT1.dataOrNull() |  | ||||||
|     @Deprecated("Use optionalT2 instead", ReplaceWith("optionalT2")) |  | ||||||
|     val t2: T2? |  | ||||||
|         get() = optionalT2.dataOrNull() |  | ||||||
|  |  | ||||||
|     companion object { |     val t1OrNull: T1? | ||||||
|         fun <T1, T2> serializer( |         get() = optionalT1.dataOrNull() | ||||||
|             t1Serializer: KSerializer<T1>, |     val t2OrNull: T2? | ||||||
|             t2Serializer: KSerializer<T2>, |         get() = optionalT2.dataOrNull() | ||||||
|         ): KSerializer<Either<T1, T2>> = EitherSerializer(t1Serializer, t2Serializer) |  | ||||||
|     } |  | ||||||
| } | } | ||||||
|  |  | ||||||
| class EitherSerializer<T1, T2>( | class EitherSerializer<T1, T2>( | ||||||
|     t1Serializer: KSerializer<T1>, |     t1Serializer: KSerializer<T1>, | ||||||
|     t2Serializer: KSerializer<T2>, |     t2Serializer: KSerializer<T2>, | ||||||
| ) : KSerializer<Either<T1, T2>> { | ) : KSerializer<Either<T1, T2>> { | ||||||
|     @OptIn(ExperimentalSerializationApi::class, InternalSerializationApi::class) |     @OptIn(InternalSerializationApi::class, ExperimentalSerializationApi::class) | ||||||
|     override val descriptor: SerialDescriptor = buildSerialDescriptor( |     override val descriptor: SerialDescriptor = buildSerialDescriptor( | ||||||
|         "TypedSerializer", |         "TypedSerializer", | ||||||
|         SerialKind.CONTEXTUAL |         SerialKind.CONTEXTUAL | ||||||
| @@ -51,7 +45,6 @@ class EitherSerializer<T1, T2>( | |||||||
|     private val t1EitherSerializer = EitherFirst.serializer(t1Serializer, t2Serializer) |     private val t1EitherSerializer = EitherFirst.serializer(t1Serializer, t2Serializer) | ||||||
|     private val t2EitherSerializer = EitherSecond.serializer(t1Serializer, t2Serializer) |     private val t2EitherSerializer = EitherSecond.serializer(t1Serializer, t2Serializer) | ||||||
|  |  | ||||||
|     @OptIn(ExperimentalSerializationApi::class, InternalSerializationApi::class) |  | ||||||
|     override fun deserialize(decoder: Decoder): Either<T1, T2> { |     override fun deserialize(decoder: Decoder): Either<T1, T2> { | ||||||
|         return decoder.decodeStructure(descriptor) { |         return decoder.decodeStructure(descriptor) { | ||||||
|             var type: String? = null |             var type: String? = null | ||||||
| @@ -83,7 +76,6 @@ class EitherSerializer<T1, T2>( | |||||||
|     } |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|     @OptIn(ExperimentalSerializationApi::class, InternalSerializationApi::class) |  | ||||||
|     override fun serialize(encoder: Encoder, value: Either<T1, T2>) { |     override fun serialize(encoder: Encoder, value: Either<T1, T2>) { | ||||||
|         encoder.encodeStructure(descriptor) { |         encoder.encodeStructure(descriptor) { | ||||||
|             when (value) { |             when (value) { | ||||||
| @@ -105,7 +97,7 @@ class EitherSerializer<T1, T2>( | |||||||
|  */ |  */ | ||||||
| @Serializable | @Serializable | ||||||
| data class EitherFirst<T1, T2>( | data class EitherFirst<T1, T2>( | ||||||
|     override val t1: T1 |     val t1: T1 | ||||||
| ) : Either<T1, T2> { | ) : Either<T1, T2> { | ||||||
|     override val optionalT1: Optional<T1> = t1.optional |     override val optionalT1: Optional<T1> = t1.optional | ||||||
|     override val optionalT2: Optional<T2> = Optional.absent() |     override val optionalT2: Optional<T2> = Optional.absent() | ||||||
| @@ -116,7 +108,7 @@ data class EitherFirst<T1, T2>( | |||||||
|  */ |  */ | ||||||
| @Serializable | @Serializable | ||||||
| data class EitherSecond<T1, T2>( | data class EitherSecond<T1, T2>( | ||||||
|     override val t2: T2 |     val t2: T2 | ||||||
| ) : Either<T1, T2> { | ) : Either<T1, T2> { | ||||||
|     override val optionalT1: Optional<T1> = Optional.absent() |     override val optionalT1: Optional<T1> = Optional.absent() | ||||||
|     override val optionalT2: Optional<T2> = t2.optional |     override val optionalT2: Optional<T2> = t2.optional | ||||||
|   | |||||||
| @@ -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) | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,135 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Contains diff based on the comparison of objects with the same [K]. | ||||||
|  |  * | ||||||
|  |  * @param removed Contains map with keys removed from parent map | ||||||
|  |  * @param changed Contains map with keys values changed new map in comparison with old one | ||||||
|  |  * @param added Contains map with new keys and values | ||||||
|  |  */ | ||||||
|  | data class MapDiff<K, V> @Warning(warning) constructor( | ||||||
|  |     val removed: Map<K, V>, | ||||||
|  |     val changed: Map<K, Pair<V, V>>, | ||||||
|  |     val added: Map<K, V> | ||||||
|  | ) { | ||||||
|  |     fun isEmpty() = removed.isEmpty() && changed.isEmpty() && added.isEmpty() | ||||||
|  |     inline fun isNotEmpty() = !isEmpty() | ||||||
|  |  | ||||||
|  |     companion object { | ||||||
|  |         private const val warning = "This feature can be changed without any warranties. Use with caution and only in case you know what you are doing" | ||||||
|  |         fun <K, V> empty() = MapDiff<K, V>(emptyMap(), emptyMap(), emptyMap()) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | private inline fun <K, V> createCompareFun( | ||||||
|  |     strictComparison: Boolean | ||||||
|  | ): (K, V, V) -> Boolean = if (strictComparison) { | ||||||
|  |     { _, first, second -> first === second } | ||||||
|  | } else { | ||||||
|  |     { _, first, second -> first == second } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Compare [this] [Map] with the [other] one in principle when [other] is newer than [this] | ||||||
|  |  * | ||||||
|  |  * @param compareFun Will be used to determine changed values | ||||||
|  |  */ | ||||||
|  | fun <K, V> Map<K, V>.diff( | ||||||
|  |     other: Map<K, V>, | ||||||
|  |     compareFun: (K, V, V) -> Boolean | ||||||
|  | ): MapDiff<K, V> { | ||||||
|  |     val removed: Map<K, V> = (keys - other.keys).associateWith { | ||||||
|  |         getValue(it) | ||||||
|  |     } | ||||||
|  |     val added: Map<K, V> = (other.keys - keys).associateWith { | ||||||
|  |         other.getValue(it) | ||||||
|  |     } | ||||||
|  |     val changed = keys.intersect(other.keys).mapNotNull { | ||||||
|  |         val old = getValue(it) | ||||||
|  |         val new = other.getValue(it) | ||||||
|  |         if (compareFun(it, old, new)) { | ||||||
|  |             return@mapNotNull null | ||||||
|  |         } else { | ||||||
|  |             it to (old to new) | ||||||
|  |         } | ||||||
|  |     }.toMap() | ||||||
|  |  | ||||||
|  |     return MapDiff( | ||||||
|  |         removed, | ||||||
|  |         changed, | ||||||
|  |         added | ||||||
|  |     ) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Compare [this] [Map] with the [other] one in principle when [other] is newer than [this] | ||||||
|  |  * | ||||||
|  |  * @param strictComparison If true, will use strict (===) comparison for the values' comparison. Otherwise, standard | ||||||
|  |  * `equals` will be used | ||||||
|  |  */ | ||||||
|  | fun <K, V> Map<K, V>.diff( | ||||||
|  |     other: Map<K, V>, | ||||||
|  |     strictComparison: Boolean = false | ||||||
|  | ): MapDiff<K, V> = diff( | ||||||
|  |     other, | ||||||
|  |     compareFun = createCompareFun(strictComparison) | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Will apply [mapDiff] to [this] [MutableMap] | ||||||
|  |  */ | ||||||
|  | fun <K, V> MutableMap<K, V>.applyDiff( | ||||||
|  |     mapDiff: MapDiff<K, V> | ||||||
|  | ) { | ||||||
|  |     mapDiff.apply { | ||||||
|  |         removed.keys.forEach { remove(it) } | ||||||
|  |         changed.forEach { (k, oldNew) -> | ||||||
|  |             put(k, oldNew.second) | ||||||
|  |         } | ||||||
|  |         added.forEach { (k, new) -> | ||||||
|  |             put(k, new) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Will apply changes with [from] map into [this] one | ||||||
|  |  * | ||||||
|  |  * @param compareFun Will be used to determine changed values | ||||||
|  |  * | ||||||
|  |  * @return [MapDiff] applied to [this] [MutableMap] | ||||||
|  |  */ | ||||||
|  | fun <K, V> MutableMap<K, V>.applyDiff( | ||||||
|  |     from: Map<K, V>, | ||||||
|  |     compareFun: (K, V, V) -> Boolean | ||||||
|  | ): MapDiff<K, V> { | ||||||
|  |     return diff(from, compareFun).also { | ||||||
|  |         applyDiff(it) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Will apply changes with [from] map into [this] one | ||||||
|  |  * | ||||||
|  |  * @param strictComparison If true, will use strict (===) comparison for the values' comparison. Otherwise, standard | ||||||
|  |  * `equals` will be used | ||||||
|  |  * | ||||||
|  |  * @return [MapDiff] applied to [this] [MutableMap] | ||||||
|  |  */ | ||||||
|  | fun <K, V> MutableMap<K, V>.applyDiff( | ||||||
|  |     from: Map<K, V>, | ||||||
|  |     strictComparison: Boolean = false | ||||||
|  | ): MapDiff<K, V> = applyDiff( | ||||||
|  |     from, | ||||||
|  |     compareFun = createCompareFun(strictComparison) | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Reverse [this] [MapDiff]. Result will contain [MapDiff.added] on [MapDiff.removed] (and vice-verse), all the | ||||||
|  |  * [MapDiff.changed] values will be reversed too | ||||||
|  |  */ | ||||||
|  | fun <K, V> MapDiff<K, V>.reversed(): MapDiff<K, V> = MapDiff( | ||||||
|  |     removed = added, | ||||||
|  |     changed = changed.mapValues { (_, oldNew) -> oldNew.second to oldNew.first }, | ||||||
|  |     added = removed | ||||||
|  | ) | ||||||
| @@ -0,0 +1,53 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | import kotlin.jvm.JvmName | ||||||
|  |  | ||||||
|  | interface SimpleMapper<T1, T2> { | ||||||
|  |     fun convertToT1(from: T2): T1 | ||||||
|  |     fun convertToT2(from: T1): T2 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @JvmName("convertFromT2") | ||||||
|  | fun <T1, T2> SimpleMapper<T1, T2>.convert(from: T2) = convertToT1(from) | ||||||
|  | @JvmName("convertFromT1") | ||||||
|  | fun <T1, T2> SimpleMapper<T1, T2>.convert(from: T1) = convertToT2(from) | ||||||
|  |  | ||||||
|  | class SimpleMapperImpl<T1, T2>( | ||||||
|  |     private val t1: (T2) -> T1, | ||||||
|  |     private val t2: (T1) -> T2, | ||||||
|  | ) : SimpleMapper<T1, T2> { | ||||||
|  |     override fun convertToT1(from: T2): T1 = t1.invoke(from) | ||||||
|  |  | ||||||
|  |     override fun convertToT2(from: T1): T2 = t2.invoke(from) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @Suppress("NOTHING_TO_INLINE") | ||||||
|  | inline fun <T1, T2> simpleMapper( | ||||||
|  |     noinline t1: (T2) -> T1, | ||||||
|  |     noinline t2: (T1) -> T2, | ||||||
|  | ) = SimpleMapperImpl(t1, t2) | ||||||
|  |  | ||||||
|  | interface SimpleSuspendableMapper<T1, T2> { | ||||||
|  |     suspend fun convertToT1(from: T2): T1 | ||||||
|  |     suspend fun convertToT2(from: T1): T2 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @JvmName("convertFromT2") | ||||||
|  | suspend fun <T1, T2> SimpleSuspendableMapper<T1, T2>.convert(from: T2) = convertToT1(from) | ||||||
|  | @JvmName("convertFromT1") | ||||||
|  | suspend fun <T1, T2> SimpleSuspendableMapper<T1, T2>.convert(from: T1) = convertToT2(from) | ||||||
|  |  | ||||||
|  | class SimpleSuspendableMapperImpl<T1, T2>( | ||||||
|  |     private val t1: suspend (T2) -> T1, | ||||||
|  |     private val t2: suspend (T1) -> T2, | ||||||
|  | ) : SimpleSuspendableMapper<T1, T2> { | ||||||
|  |     override suspend fun convertToT1(from: T2): T1 = t1.invoke(from) | ||||||
|  |  | ||||||
|  |     override suspend fun convertToT2(from: T1): T2 = t2.invoke(from) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @Suppress("NOTHING_TO_INLINE") | ||||||
|  | inline fun <T1, T2> simpleSuspendableMapper( | ||||||
|  |     noinline t1: suspend (T2) -> T1, | ||||||
|  |     noinline t2: suspend (T1) -> T2, | ||||||
|  | ) = SimpleSuspendableMapperImpl(t1, t2) | ||||||
| @@ -41,10 +41,18 @@ data class Optional<T> internal constructor( | |||||||
| inline val <T> T.optional | inline val <T> T.optional | ||||||
|     get() = Optional.presented(this) |     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) |  * Will call [block] when data presented ([Optional.dataPresented] == true) | ||||||
|  */ |  */ | ||||||
| inline fun <T> Optional<T>.onPresented(block: (T) -> Unit): Optional<T> = apply { | inline fun <T> Optional<T>.onPresented(block: (T) -> Unit): Optional<T> = apply { | ||||||
|  |     @OptIn(Warning::class) | ||||||
|     if (dataPresented) { @Suppress("UNCHECKED_CAST") block(data as T) } |     if (dataPresented) { @Suppress("UNCHECKED_CAST") block(data as T) } | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -52,6 +60,7 @@ inline fun <T> Optional<T>.onPresented(block: (T) -> Unit): Optional<T> = apply | |||||||
|  * Will call [block] when data presented ([Optional.dataPresented] == true) |  * Will call [block] when data presented ([Optional.dataPresented] == true) | ||||||
|  */ |  */ | ||||||
| inline fun <T, R> Optional<T>.mapOnPresented(block: (T) -> R): R? = run { | 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 |     if (dataPresented) { @Suppress("UNCHECKED_CAST") block(data as T) } else null | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -59,6 +68,7 @@ inline fun <T, R> Optional<T>.mapOnPresented(block: (T) -> R): R? = run { | |||||||
|  * Will call [block] when data absent ([Optional.dataPresented] == false) |  * Will call [block] when data absent ([Optional.dataPresented] == false) | ||||||
|  */ |  */ | ||||||
| inline fun <T> Optional<T>.onAbsent(block: () -> Unit): Optional<T> = apply { | inline fun <T> Optional<T>.onAbsent(block: () -> Unit): Optional<T> = apply { | ||||||
|  |     @OptIn(Warning::class) | ||||||
|     if (!dataPresented) { block() } |     if (!dataPresented) { block() } | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -66,27 +76,22 @@ inline fun <T> Optional<T>.onAbsent(block: () -> Unit): Optional<T> = apply { | |||||||
|  * Will call [block] when data presented ([Optional.dataPresented] == true) |  * Will call [block] when data presented ([Optional.dataPresented] == true) | ||||||
|  */ |  */ | ||||||
| inline fun <T, R> Optional<T>.mapOnAbsent(block: () -> R): R? = run { | inline fun <T, R> Optional<T>.mapOnAbsent(block: () -> R): R? = run { | ||||||
|     if (!dataPresented) { @Suppress("UNCHECKED_CAST") block() } else null |     @OptIn(Warning::class) | ||||||
|  |     if (!dataPresented) { block() } else null | ||||||
| } | } | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Returns [Optional.data] if [Optional.dataPresented] of [this] is true, or null otherwise |  * Returns [Optional.data] if [Optional.dataPresented] of [this] is true, or null otherwise | ||||||
|  */ |  */ | ||||||
| fun <T> Optional<T>.dataOrNull() = if (dataPresented) @Suppress("UNCHECKED_CAST") (data as T) else null | 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 |  * Returns [Optional.data] if [Optional.dataPresented] of [this] is true, or throw [throwable] otherwise | ||||||
|  */ |  */ | ||||||
| fun <T> Optional<T>.dataOrThrow(throwable: Throwable) = if (dataPresented) @Suppress("UNCHECKED_CAST") (data as T) else throw throwable | 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 |  * 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) = if (dataPresented) @Suppress("UNCHECKED_CAST") (data as T) else block() | inline fun <T> Optional<T>.dataOrElse(block: () -> T) = @OptIn(Warning::class) if (dataPresented) @Suppress("UNCHECKED_CAST") (data as T) else block() | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Returns [Optional.data] if [Optional.dataPresented] of [this] is true, or call [block] and returns the result of it |  | ||||||
|  */ |  | ||||||
| @Deprecated("dataOrElse now is inline", ReplaceWith("dataOrElse", "dev.inmo.micro_utils.common.dataOrElse")) |  | ||||||
| suspend fun <T> Optional<T>.dataOrElseSuspendable(block: suspend () -> T) = 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() | ||||||
| @@ -1,5 +1,27 @@ | |||||||
| package dev.inmo.micro_utils.common | 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]. |  * Executes the given [action] until getting of successful result specified number of [times]. | ||||||
|  * |  * | ||||||
| @@ -10,12 +32,23 @@ inline fun <R> repeatOnFailure( | |||||||
|     onEachFailure: (Throwable) -> Unit = {}, |     onEachFailure: (Throwable) -> Unit = {}, | ||||||
|     action: (Int) -> R |     action: (Int) -> R | ||||||
| ): Optional<R> { | ): Optional<R> { | ||||||
|     repeat(times) { |     var i = 0 | ||||||
|         runCatching { |     val result = repeatOnFailure( | ||||||
|             action(it) |         { | ||||||
|         }.onFailure(onEachFailure).onSuccess { |             onEachFailure(it) | ||||||
|             return Optional.presented(it) |             if (i < times) { | ||||||
|  |                 i++ | ||||||
|  |                 true | ||||||
|  |             } else { | ||||||
|  |                 false | ||||||
|  |             } | ||||||
|         } |         } | ||||||
|  |     ) { | ||||||
|  |         action(i) | ||||||
|  |     } | ||||||
|  |     return if (result.isSuccess) { | ||||||
|  |         Optional.presented(result.getOrThrow()) | ||||||
|  |     } else { | ||||||
|  |         Optional.absent() | ||||||
|     } |     } | ||||||
|     return 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 | ||||||
| @@ -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 | ||||||
|                 } |                 } | ||||||
| @@ -55,7 +55,7 @@ class DiffUtilsTests { | |||||||
|         val withIndex = oldList.withIndex() |         val withIndex = oldList.withIndex() | ||||||
|  |  | ||||||
|         for (step in oldList.indices) { |         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) | ||||||
| @@ -104,7 +104,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 | ||||||
|                 } |                 } | ||||||
| @@ -129,15 +129,20 @@ class DiffUtilsTests { | |||||||
|         val withIndex = oldList.withIndex() |         val withIndex = oldList.withIndex() | ||||||
|  |  | ||||||
|         for (step in oldList.indices) { |         for (step in oldList.indices) { | ||||||
|             for ((i, v) in withIndex) { |             for ((i, _) in withIndex) { | ||||||
|                 val mutable = oldList.toMutableList() |                 val mutable = oldList.toMutableList() | ||||||
|                 val changes = ( |  | ||||||
|                     if (step == 0) i until oldList.size else (i until oldList.size step step) |                 val newList = if (step == 0) { | ||||||
|                 ).map { index -> |                     i until oldList.size | ||||||
|  |                 } else { | ||||||
|  |                     i until oldList.size step step | ||||||
|  |                 } | ||||||
|  |                 newList.forEach { index -> | ||||||
|                     IndexedValue(index, mutable[index]) to IndexedValue(index, "changed$index").also { |                     IndexedValue(index, mutable[index]) to IndexedValue(index, "changed$index").also { | ||||||
|                         mutable[index] = it.value |                         mutable[index] = it.value | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|  |  | ||||||
|                 val mutableOldList = oldList.toMutableList() |                 val mutableOldList = oldList.toMutableList() | ||||||
|                 mutableOldList.applyDiff(mutable) |                 mutableOldList.applyDiff(mutable) | ||||||
|                 assertEquals( |                 assertEquals( | ||||||
|   | |||||||
| @@ -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,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,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,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,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,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/linuxArm64Main/kotlin/ActualMPPFile.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								common/src/linuxArm64Main/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() | ||||||
|  |     } | ||||||
							
								
								
									
										25
									
								
								common/src/linuxArm64Main/kotlin/fixed.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								common/src/linuxArm64Main/kotlin/fixed.kt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | import kotlinx.cinterop.* | ||||||
|  | import platform.posix.snprintf | ||||||
|  | import platform.posix.sprintf | ||||||
|  |  | ||||||
|  | @OptIn(ExperimentalForeignApi::class) | ||||||
|  | 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() | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @OptIn(ExperimentalForeignApi::class) | ||||||
|  | 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() | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										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() | ||||||
|  |     } | ||||||
							
								
								
									
										25
									
								
								common/src/linuxX64Main/kotlin/fixed.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								common/src/linuxX64Main/kotlin/fixed.kt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | import kotlinx.cinterop.* | ||||||
|  | import platform.posix.snprintf | ||||||
|  | import platform.posix.sprintf | ||||||
|  |  | ||||||
|  | @OptIn(ExperimentalForeignApi::class) | ||||||
|  | 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() | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @OptIn(ExperimentalForeignApi::class) | ||||||
|  | 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() | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -1 +0,0 @@ | |||||||
| <manifest package="dev.inmo.micro_utils.common"/> |  | ||||||
							
								
								
									
										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() | ||||||
|  |     } | ||||||
							
								
								
									
										25
									
								
								common/src/mingwX64Main/kotlin/fixed.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								common/src/mingwX64Main/kotlin/fixed.kt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | import kotlinx.cinterop.* | ||||||
|  | import platform.posix.snprintf | ||||||
|  | import platform.posix.sprintf | ||||||
|  |  | ||||||
|  | @OptIn(ExperimentalForeignApi::class) | ||||||
|  | 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() | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @OptIn(ExperimentalForeignApi::class) | ||||||
|  | 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() | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -4,7 +4,7 @@ plugins { | |||||||
|     id "com.android.library" |     id "com.android.library" | ||||||
| } | } | ||||||
|  |  | ||||||
| apply from: "$mppProjectWithSerializationPresetPath" | apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64ProjectPresetPath" | ||||||
|  |  | ||||||
| kotlin { | kotlin { | ||||||
|     sourceSets { |     sourceSets { | ||||||
| @@ -22,6 +22,7 @@ kotlin { | |||||||
|             dependencies { |             dependencies { | ||||||
|                 api libs.kt.coroutines.android |                 api libs.kt.coroutines.android | ||||||
|             } |             } | ||||||
|  |             dependsOn(jvmMain) | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -13,6 +13,7 @@ kotlin { | |||||||
|             dependencies { |             dependencies { | ||||||
|                 api libs.kt.coroutines |                 api libs.kt.coroutines | ||||||
|                 api project(":micro_utils.coroutines") |                 api project(":micro_utils.coroutines") | ||||||
|  |                 api project(":micro_utils.common.compose") | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|   | |||||||
							
								
								
									
										1
									
								
								coroutines/compose/src/androidMain/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								coroutines/compose/src/androidMain/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | <manifest/> | ||||||
| @@ -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,46 @@ | |||||||
|  | package dev.inmo.micro_utils.coroutines.compose | ||||||
|  |  | ||||||
|  | import androidx.compose.runtime.MutableState | ||||||
|  | import dev.inmo.micro_utils.coroutines.SpecialMutableStateFlow | ||||||
|  | import kotlinx.coroutines.CoroutineScope | ||||||
|  | import kotlinx.coroutines.Dispatchers | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * This type works like [MutableState], [kotlinx.coroutines.flow.StateFlow] and [kotlinx.coroutines.flow.MutableSharedFlow]. | ||||||
|  |  * Based on [SpecialMutableStateFlow] | ||||||
|  |  */ | ||||||
|  | @Deprecated("Will be removed soon") | ||||||
|  | class FlowState<T>( | ||||||
|  |     initial: T, | ||||||
|  |     internalScope: CoroutineScope = CoroutineScope(Dispatchers.Default) | ||||||
|  | ) : MutableState<T>, | ||||||
|  |     SpecialMutableStateFlow<T>(initial, internalScope) { | ||||||
|  |     private var internalValue: T = initial | ||||||
|  |     override var value: T | ||||||
|  |         get() = internalValue | ||||||
|  |         set(value) { | ||||||
|  |             internalValue = value | ||||||
|  |             tryEmit(value) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |     override fun onChangeWithoutSync(value: T) { | ||||||
|  |         internalValue = value | ||||||
|  |         super.onChangeWithoutSync(value) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun component1(): T = value | ||||||
|  |  | ||||||
|  |     override fun component2(): (T) -> Unit = { tryEmit(it) } | ||||||
|  |  | ||||||
|  |     override fun tryEmit(value: T): Boolean { | ||||||
|  |         internalValue = value | ||||||
|  |         return super.tryEmit(value) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override suspend fun emit(value: T) { | ||||||
|  |         internalValue = value | ||||||
|  |         super.emit(value) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | //fun <T> MutableState<T>.asFlowState(scope: CoroutineScope = CoroutineScope(Dispatchers.Main)) = FlowState(this, scope) | ||||||
| @@ -1,22 +0,0 @@ | |||||||
| package dev.inmo.micro_utils.coroutines.compose |  | ||||||
|  |  | ||||||
| import androidx.compose.runtime.MutableState |  | ||||||
| import androidx.compose.runtime.mutableStateOf |  | ||||||
| import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions |  | ||||||
| import kotlinx.coroutines.CoroutineScope |  | ||||||
| import kotlinx.coroutines.flow.Flow |  | ||||||
| import kotlinx.coroutines.flow.StateFlow |  | ||||||
|  |  | ||||||
| fun <T> Flow<T>.toMutableState( |  | ||||||
|     initial: T, |  | ||||||
|     scope: CoroutineScope |  | ||||||
| ): MutableState<T> { |  | ||||||
|     val state = mutableStateOf(initial) |  | ||||||
|     subscribeSafelyWithoutExceptions(scope) { state.value = it } |  | ||||||
|     return state |  | ||||||
| } |  | ||||||
|  |  | ||||||
| inline fun <T> StateFlow<T>.toMutableState( |  | ||||||
|     scope: CoroutineScope |  | ||||||
| ): MutableState<T> = toMutableState(value, scope) |  | ||||||
|  |  | ||||||
| @@ -1,6 +1,7 @@ | |||||||
| package dev.inmo.micro_utils.coroutines.compose | package dev.inmo.micro_utils.coroutines.compose | ||||||
|  |  | ||||||
| import androidx.compose.runtime.* | import androidx.compose.runtime.* | ||||||
|  | import dev.inmo.micro_utils.common.compose.linkWithElement | ||||||
| import kotlinx.coroutines.* | import kotlinx.coroutines.* | ||||||
| import org.jetbrains.compose.web.dom.DOMScope | import org.jetbrains.compose.web.dom.DOMScope | ||||||
| import org.w3c.dom.Element | import org.w3c.dom.Element | ||||||
| @@ -14,3 +15,12 @@ suspend fun <TElement : Element> renderComposableAndLinkToContext( | |||||||
|         currentCoroutineContext() |         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 +0,0 @@ | |||||||
| <manifest package="dev.inmo.micro_utils.coroutines.compose"/> |  | ||||||
							
								
								
									
										1
									
								
								coroutines/src/androidMain/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								coroutines/src/androidMain/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | <manifest/> | ||||||
| @@ -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) | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -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) | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -6,11 +6,12 @@ import kotlinx.coroutines.channels.Channel | |||||||
| import kotlinx.coroutines.flow.* | import kotlinx.coroutines.flow.* | ||||||
| import kotlinx.coroutines.sync.Mutex | import kotlinx.coroutines.sync.Mutex | ||||||
| import kotlinx.coroutines.sync.withLock | import kotlinx.coroutines.sync.withLock | ||||||
|  | import kotlin.coroutines.cancellation.CancellationException | ||||||
|  |  | ||||||
| private sealed interface AccumulatorFlowStep | private sealed interface AccumulatorFlowStep<T> | ||||||
| private data class DataRetrievedAccumulatorFlowStep(val data: Any) : AccumulatorFlowStep | private data class DataRetrievedAccumulatorFlowStep<T>(val data: T) : AccumulatorFlowStep<T> | ||||||
| private data class SubscribeAccumulatorFlowStep(val channel: Channel<Any>) : AccumulatorFlowStep | private data class SubscribeAccumulatorFlowStep<T>(val channel: Channel<T>) : AccumulatorFlowStep<T> | ||||||
| private data class UnsubscribeAccumulatorFlowStep(val channel: Channel<Any>) : AccumulatorFlowStep | 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: |  * This [Flow] will have behaviour very similar to [SharedFlow], but there are several differences: | ||||||
| @@ -26,12 +27,12 @@ class AccumulatorFlow<T>( | |||||||
|     private val subscope = scope.LinkedSupervisorScope() |     private val subscope = scope.LinkedSupervisorScope() | ||||||
|     private val activeData = ArrayDeque<T>() |     private val activeData = ArrayDeque<T>() | ||||||
|     private val dataMutex = Mutex() |     private val dataMutex = Mutex() | ||||||
|     private val channelsForBroadcast = mutableListOf<Channel<Any>>() |     private val channelsForBroadcast = mutableListOf<Channel<T>>() | ||||||
|     private val channelsMutex = Mutex() |     private val channelsMutex = Mutex() | ||||||
|     private val steps = subscope.actor<AccumulatorFlowStep> { step -> |     private val steps = subscope.actor<AccumulatorFlowStep<T>> { step -> | ||||||
|         when (step) { |         when (step) { | ||||||
|             is DataRetrievedAccumulatorFlowStep -> { |             is DataRetrievedAccumulatorFlowStep -> { | ||||||
|                 if (activeData.first() === step.data) { |                 if (activeData.firstOrNull() === step.data) { | ||||||
|                     dataMutex.withLock { |                     dataMutex.withLock { | ||||||
|                         activeData.removeFirst() |                         activeData.removeFirst() | ||||||
|                     } |                     } | ||||||
| @@ -42,7 +43,7 @@ class AccumulatorFlow<T>( | |||||||
|                 dataMutex.withLock { |                 dataMutex.withLock { | ||||||
|                     val dataToSend = activeData.toList() |                     val dataToSend = activeData.toList() | ||||||
|                     safelyWithoutExceptions { |                     safelyWithoutExceptions { | ||||||
|                         dataToSend.forEach { step.channel.send(it as Any) } |                         dataToSend.forEach { step.channel.send(it) } | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
| @@ -58,24 +59,29 @@ class AccumulatorFlow<T>( | |||||||
|         channelsMutex.withLock { |         channelsMutex.withLock { | ||||||
|             channelsForBroadcast.forEach { channel -> |             channelsForBroadcast.forEach { channel -> | ||||||
|                 safelyWithResult { |                 safelyWithResult { | ||||||
|                     channel.send(it as Any) |                     channel.send(it) | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     override suspend fun collectSafely(collector: FlowCollector<T>) { |     override suspend fun collectSafely(collector: FlowCollector<T>) { | ||||||
|         val channel = Channel<Any>(Channel.UNLIMITED, BufferOverflow.SUSPEND) |         val channel = Channel<T>(Channel.UNLIMITED, BufferOverflow.SUSPEND) | ||||||
|         steps.send(SubscribeAccumulatorFlowStep(channel)) |         steps.send(SubscribeAccumulatorFlowStep(channel)) | ||||||
|         for (data in channel) { |         val result = runCatchingSafely { | ||||||
|             try { |             for (data in channel) { | ||||||
|                 collector.emit(data as T) |                 val emitResult = runCatchingSafely { | ||||||
|                 steps.send(DataRetrievedAccumulatorFlowStep(data)) |                     collector.emit(data) | ||||||
|             } finally { |                 } | ||||||
|                 channel.cancel() |                 if (emitResult.isSuccess || emitResult.exceptionOrNull() is CancellationException) { | ||||||
|                 steps.send(UnsubscribeAccumulatorFlowStep(channel)) |                     steps.send(DataRetrievedAccumulatorFlowStep(data)) | ||||||
|  |                 } | ||||||
|  |                 emitResult.getOrThrow() | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |         channel.cancel() | ||||||
|  |         steps.send(UnsubscribeAccumulatorFlowStep(channel)) | ||||||
|  |         result.getOrThrow() | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -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,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() | ||||||
| @@ -115,10 +115,21 @@ suspend inline fun <T> runCatchingSafely( | |||||||
|     safely(onException, block) |     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( | suspend inline fun <T> safelyWithResult( | ||||||
|     noinline block: suspend CoroutineScope.() -> T |     noinline block: suspend CoroutineScope.() -> T | ||||||
| ): Result<T> = runCatchingSafely(defaultSafelyExceptionHandler, block) | ): 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 |  * Use this handler in cases you wish to include handling of exceptions by [defaultSafelyWithoutExceptionHandler] and | ||||||
|  * returning null at one time |  * returning null at one time | ||||||
|   | |||||||
| @@ -0,0 +1,136 @@ | |||||||
|  | package dev.inmo.micro_utils.coroutines | ||||||
|  |  | ||||||
|  | import kotlinx.coroutines.currentCoroutineContext | ||||||
|  | import kotlinx.coroutines.flow.MutableStateFlow | ||||||
|  | import kotlinx.coroutines.flow.StateFlow | ||||||
|  | import kotlinx.coroutines.flow.asStateFlow | ||||||
|  | import kotlinx.coroutines.flow.first | ||||||
|  | import kotlinx.coroutines.isActive | ||||||
|  | import kotlinx.coroutines.sync.Mutex | ||||||
|  | import kotlinx.coroutines.sync.withLock | ||||||
|  | import kotlin.contracts.ExperimentalContracts | ||||||
|  | import kotlin.contracts.InvocationKind | ||||||
|  | import kotlin.contracts.contract | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * It is interface which will work like classic [Mutex], but in difference have [lockStateFlow] for listening of the | ||||||
|  |  * [SmartMutex] state. | ||||||
|  |  * | ||||||
|  |  * There is [Mutable] and [Immutable] realizations. In case you are owner and manager current state of lock, you need | ||||||
|  |  * [Mutable] [SmartMutex]. Otherwise, [Immutable]. | ||||||
|  |  * | ||||||
|  |  * Any [Mutable] [SmartMutex] may produce its [Immutable] variant which will contains [lockStateFlow] equal to its | ||||||
|  |  * [Mutable] creator | ||||||
|  |  */ | ||||||
|  | sealed interface SmartMutex { | ||||||
|  |     val lockStateFlow: StateFlow<Boolean> | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * * True - locked | ||||||
|  |      * * False - unlocked | ||||||
|  |      */ | ||||||
|  |     val isLocked: Boolean | ||||||
|  |         get() = lockStateFlow.value | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Immutable variant of [SmartMutex]. In fact will depend on the owner of [lockStateFlow] | ||||||
|  |      */ | ||||||
|  |     class Immutable(override val lockStateFlow: StateFlow<Boolean>) : SmartMutex | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Mutable variant of [SmartMutex]. With that variant you may [lock] and [unlock]. Besides, you may create | ||||||
|  |      * [Immutable] variant of [this] instance with [immutable] factory | ||||||
|  |      * | ||||||
|  |      * @param locked Preset state of [isLocked] and its internal [_lockStateFlow] | ||||||
|  |      */ | ||||||
|  |     class Mutable(locked: Boolean = false) : SmartMutex { | ||||||
|  |         private val _lockStateFlow = MutableStateFlow<Boolean>(locked) | ||||||
|  |         override val lockStateFlow: StateFlow<Boolean> = _lockStateFlow.asStateFlow() | ||||||
|  |  | ||||||
|  |         private val internalChangesMutex = Mutex() | ||||||
|  |  | ||||||
|  |         fun immutable() = Immutable(lockStateFlow) | ||||||
|  |  | ||||||
|  |         /** | ||||||
|  |          * Holds call until this [SmartMutex] will be re-locked. That means that while [isLocked] == true, [holds] will | ||||||
|  |          * wait for [isLocked] == false and then try to lock | ||||||
|  |          */ | ||||||
|  |         suspend fun lock() { | ||||||
|  |             do { | ||||||
|  |                 waitUnlock() | ||||||
|  |                 val shouldContinue = internalChangesMutex.withLock { | ||||||
|  |                     if (_lockStateFlow.value) { | ||||||
|  |                         true | ||||||
|  |                     } else { | ||||||
|  |                         _lockStateFlow.value = true | ||||||
|  |                         false | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } while (shouldContinue && currentCoroutineContext().isActive) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         /** | ||||||
|  |          * Will try to lock this [SmartMutex] immediataly | ||||||
|  |          * | ||||||
|  |          * @return True if lock was successful. False otherwise | ||||||
|  |          */ | ||||||
|  |         suspend fun tryLock(): Boolean { | ||||||
|  |             return if (!_lockStateFlow.value) { | ||||||
|  |                 internalChangesMutex.withLock { | ||||||
|  |                     if (!_lockStateFlow.value) { | ||||||
|  |                         _lockStateFlow.value = true | ||||||
|  |                         true | ||||||
|  |                     } else { | ||||||
|  |                         false | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } else { | ||||||
|  |                 false | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         /** | ||||||
|  |          * If [isLocked] == true - will change it to false and return true. If current call will not unlock this | ||||||
|  |          * [SmartMutex] - false | ||||||
|  |          */ | ||||||
|  |         suspend fun unlock(): Boolean { | ||||||
|  |             return if (_lockStateFlow.value) { | ||||||
|  |                 internalChangesMutex.withLock { | ||||||
|  |                     if (_lockStateFlow.value) { | ||||||
|  |                         _lockStateFlow.value = false | ||||||
|  |                         true | ||||||
|  |                     } else { | ||||||
|  |                         false | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } else { | ||||||
|  |                 false | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Will call [SmartMutex.Mutable.lock], then execute [action] and return the result after [SmartMutex.Mutable.unlock] | ||||||
|  |  */ | ||||||
|  | @OptIn(ExperimentalContracts::class) | ||||||
|  | suspend inline fun <T> SmartMutex.Mutable.withLock(action: () -> T): T { | ||||||
|  |     contract { | ||||||
|  |         callsInPlace(action, InvocationKind.EXACTLY_ONCE) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     lock() | ||||||
|  |     try { | ||||||
|  |         return action() | ||||||
|  |     } finally { | ||||||
|  |         unlock() | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Will wait until the [SmartMutex.lockStateFlow] of [this] instance will be false. | ||||||
|  |  * | ||||||
|  |  * Anyway, after the end of this block there are no any guaranties that [SmartMutex.isLocked] == false due to the fact | ||||||
|  |  * that some other parties may lock it again | ||||||
|  |  */ | ||||||
|  | suspend fun SmartMutex.waitUnlock() = lockStateFlow.first { !it } | ||||||
| @@ -0,0 +1,105 @@ | |||||||
|  | package dev.inmo.micro_utils.coroutines | ||||||
|  |  | ||||||
|  | import kotlin.contracts.ExperimentalContracts | ||||||
|  | import kotlin.contracts.InvocationKind | ||||||
|  | import kotlin.contracts.contract | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Composite mutex which works with next rules: | ||||||
|  |  * | ||||||
|  |  * * [acquireRead] require to [writeMutex] be free. Then it will take one lock from [readSemaphore] | ||||||
|  |  * * [releaseRead] will just free up one permit in [readSemaphore] | ||||||
|  |  * * [lockWrite] will lock [writeMutex] and then await while all [readSemaphore] will be freed | ||||||
|  |  * * [unlockWrite] will just unlock [writeMutex] | ||||||
|  |  */ | ||||||
|  | class SmartRWLocker(private val readPermits: Int = Int.MAX_VALUE, writeIsLocked: Boolean = false) { | ||||||
|  |     private val _readSemaphore = SmartSemaphore.Mutable(permits = readPermits, acquiredPermits = 0) | ||||||
|  |     private val _writeMutex = SmartMutex.Mutable(locked = writeIsLocked) | ||||||
|  |  | ||||||
|  |     val readSemaphore: SmartSemaphore.Immutable = _readSemaphore.immutable() | ||||||
|  |     val writeMutex: SmartMutex.Immutable = _writeMutex.immutable() | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Do lock in [readSemaphore] inside of [writeMutex] locking | ||||||
|  |      */ | ||||||
|  |     suspend fun acquireRead() { | ||||||
|  |         _writeMutex.waitUnlock() | ||||||
|  |         _readSemaphore.acquire() | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Release one read permit in [readSemaphore] | ||||||
|  |      */ | ||||||
|  |     suspend fun releaseRead(): Boolean { | ||||||
|  |         return _readSemaphore.release() | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Locking [writeMutex] and wait while all [readSemaphore] permits will be freed | ||||||
|  |      */ | ||||||
|  |     suspend fun lockWrite() { | ||||||
|  |         _writeMutex.lock() | ||||||
|  |         _readSemaphore.acquire(readPermits) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Unlock [writeMutex] | ||||||
|  |      */ | ||||||
|  |     suspend fun unlockWrite(): Boolean { | ||||||
|  |         return _writeMutex.unlock().also { | ||||||
|  |             if (it) { | ||||||
|  |                 _readSemaphore.release(readPermits) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Will call [SmartSemaphore.Mutable.lock], then execute [action] and return the result after [SmartSemaphore.Mutable.unlock] | ||||||
|  |  */ | ||||||
|  | @OptIn(ExperimentalContracts::class) | ||||||
|  | suspend inline fun <T> SmartRWLocker.withReadAcquire(action: () -> T): T { | ||||||
|  |     contract { | ||||||
|  |         callsInPlace(action, InvocationKind.EXACTLY_ONCE) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     acquireRead() | ||||||
|  |     try { | ||||||
|  |         return action() | ||||||
|  |     } finally { | ||||||
|  |         releaseRead() | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Will wait until the [SmartSemaphore.permitsStateFlow] of [this] instance will have [permits] count free permits. | ||||||
|  |  * | ||||||
|  |  * Anyway, after the end of this block there are no any guaranties that [SmartSemaphore.freePermits] >= [permits] due to | ||||||
|  |  * the fact that some other parties may lock it again | ||||||
|  |  */ | ||||||
|  | suspend fun SmartRWLocker.waitReadRelease(permits: Int = 1) = readSemaphore.waitRelease(permits) | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Will call [SmartMutex.Mutable.lock], then execute [action] and return the result after [SmartMutex.Mutable.unlock] | ||||||
|  |  */ | ||||||
|  | @OptIn(ExperimentalContracts::class) | ||||||
|  | suspend inline fun <T> SmartRWLocker.withWriteLock(action: () -> T): T { | ||||||
|  |     contract { | ||||||
|  |         callsInPlace(action, InvocationKind.EXACTLY_ONCE) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     lockWrite() | ||||||
|  |     try { | ||||||
|  |         return action() | ||||||
|  |     } finally { | ||||||
|  |         unlockWrite() | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Will wait until the [SmartMutex.lockStateFlow] of [this] instance will be false. | ||||||
|  |  * | ||||||
|  |  * Anyway, after the end of this block there are no any guaranties that [SmartMutex.isLocked] == false due to the fact | ||||||
|  |  * that some other parties may lock it again | ||||||
|  |  */ | ||||||
|  | suspend fun SmartRWLocker.waitWriteUnlock() = writeMutex.waitUnlock() | ||||||
| @@ -0,0 +1,168 @@ | |||||||
|  | package dev.inmo.micro_utils.coroutines | ||||||
|  |  | ||||||
|  | import kotlinx.coroutines.currentCoroutineContext | ||||||
|  | import kotlinx.coroutines.flow.MutableStateFlow | ||||||
|  | import kotlinx.coroutines.flow.StateFlow | ||||||
|  | import kotlinx.coroutines.flow.asStateFlow | ||||||
|  | import kotlinx.coroutines.flow.first | ||||||
|  | import kotlinx.coroutines.isActive | ||||||
|  | import kotlinx.coroutines.sync.Mutex | ||||||
|  | import kotlinx.coroutines.sync.Semaphore | ||||||
|  | import kotlinx.coroutines.sync.withLock | ||||||
|  | import kotlin.contracts.ExperimentalContracts | ||||||
|  | import kotlin.contracts.InvocationKind | ||||||
|  | import kotlin.contracts.contract | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * It is interface which will work like classic [Semaphore], but in difference have [permitsStateFlow] for listening of the | ||||||
|  |  * [SmartSemaphore] state. | ||||||
|  |  * | ||||||
|  |  * There is [Mutable] and [Immutable] realizations. In case you are owner and manager current state of lock, you need | ||||||
|  |  * [Mutable] [SmartSemaphore]. Otherwise, [Immutable]. | ||||||
|  |  * | ||||||
|  |  * Any [Mutable] [SmartSemaphore] may produce its [Immutable] variant which will contains [permitsStateFlow] equal to its | ||||||
|  |  * [Mutable] creator | ||||||
|  |  */ | ||||||
|  | sealed interface SmartSemaphore { | ||||||
|  |     val permitsStateFlow: StateFlow<Int> | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * * True - locked | ||||||
|  |      * * False - unlocked | ||||||
|  |      */ | ||||||
|  |     val freePermits: Int | ||||||
|  |         get() = permitsStateFlow.value | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Immutable variant of [SmartSemaphore]. In fact will depend on the owner of [permitsStateFlow] | ||||||
|  |      */ | ||||||
|  |     class Immutable(override val permitsStateFlow: StateFlow<Int>) : SmartSemaphore | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Mutable variant of [SmartSemaphore]. With that variant you may [lock] and [unlock]. Besides, you may create | ||||||
|  |      * [Immutable] variant of [this] instance with [immutable] factory | ||||||
|  |      * | ||||||
|  |      * @param locked Preset state of [freePermits] and its internal [_freePermitsStateFlow] | ||||||
|  |      */ | ||||||
|  |     class Mutable(private val permits: Int, acquiredPermits: Int = 0) : SmartSemaphore { | ||||||
|  |         private val _freePermitsStateFlow = MutableStateFlow<Int>(permits - acquiredPermits) | ||||||
|  |         override val permitsStateFlow: StateFlow<Int> = _freePermitsStateFlow.asStateFlow() | ||||||
|  |  | ||||||
|  |         private val internalChangesMutex = Mutex(false) | ||||||
|  |  | ||||||
|  |         fun immutable() = Immutable(permitsStateFlow) | ||||||
|  |  | ||||||
|  |         private fun checkedPermits(permits: Int) = permits.coerceIn(1 .. this.permits) | ||||||
|  |  | ||||||
|  |         /** | ||||||
|  |          * Holds call until this [SmartSemaphore] will be re-locked. That means that current method will | ||||||
|  |          */ | ||||||
|  |         suspend fun acquire(permits: Int = 1) { | ||||||
|  |             var acquiredPermits = 0 | ||||||
|  |             val checkedPermits = checkedPermits(permits) | ||||||
|  |             try { | ||||||
|  |                 do { | ||||||
|  |                     val shouldContinue = internalChangesMutex.withLock { | ||||||
|  |                         val requiredPermits = checkedPermits - acquiredPermits | ||||||
|  |                         val acquiring = minOf(freePermits, requiredPermits).takeIf { it > 0 } ?: return@withLock true | ||||||
|  |                         acquiredPermits += acquiring | ||||||
|  |                         _freePermitsStateFlow.value -= acquiring | ||||||
|  |  | ||||||
|  |                         acquiredPermits != checkedPermits | ||||||
|  |                     } | ||||||
|  |                     if (shouldContinue) { | ||||||
|  |                         waitRelease() | ||||||
|  |                     } | ||||||
|  |                 } while (shouldContinue && currentCoroutineContext().isActive) | ||||||
|  |             } catch (e: Throwable) { | ||||||
|  |                 release(acquiredPermits) | ||||||
|  |                 throw e | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         /** | ||||||
|  |          * Holds call until this [SmartSemaphore] will be re-locked. That means that while [freePermits] == true, [holds] will | ||||||
|  |          * wait for [freePermits] == false and then try to lock | ||||||
|  |          */ | ||||||
|  |         suspend fun acquireByOne(permits: Int = 1) { | ||||||
|  |             val checkedPermits = checkedPermits(permits) | ||||||
|  |             do { | ||||||
|  |                 waitRelease(checkedPermits) | ||||||
|  |                 val shouldContinue = internalChangesMutex.withLock { | ||||||
|  |                     if (_freePermitsStateFlow.value < checkedPermits) { | ||||||
|  |                         true | ||||||
|  |                     } else { | ||||||
|  |                         _freePermitsStateFlow.value -= checkedPermits | ||||||
|  |                         false | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } while (shouldContinue && currentCoroutineContext().isActive) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         /** | ||||||
|  |          * Will try to lock this [SmartSemaphore] immediataly | ||||||
|  |          * | ||||||
|  |          * @return True if lock was successful. False otherwise | ||||||
|  |          */ | ||||||
|  |         suspend fun tryAcquire(permits: Int = 1): Boolean { | ||||||
|  |             val checkedPermits = checkedPermits(permits) | ||||||
|  |             return if (_freePermitsStateFlow.value < checkedPermits) { | ||||||
|  |                 internalChangesMutex.withLock { | ||||||
|  |                     if (_freePermitsStateFlow.value < checkedPermits) { | ||||||
|  |                         _freePermitsStateFlow.value -= checkedPermits | ||||||
|  |                         true | ||||||
|  |                     } else { | ||||||
|  |                         false | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } else { | ||||||
|  |                 false | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         /** | ||||||
|  |          * If [freePermits] == true - will change it to false and return true. If current call will not unlock this | ||||||
|  |          * [SmartSemaphore] - false | ||||||
|  |          */ | ||||||
|  |         suspend fun release(permits: Int = 1): Boolean { | ||||||
|  |             val checkedPermits = checkedPermits(permits) | ||||||
|  |             return if (_freePermitsStateFlow.value < this.permits) { | ||||||
|  |                 internalChangesMutex.withLock { | ||||||
|  |                     if (_freePermitsStateFlow.value < this.permits) { | ||||||
|  |                         _freePermitsStateFlow.value = minOf(_freePermitsStateFlow.value + checkedPermits, this.permits) | ||||||
|  |                         true | ||||||
|  |                     } else { | ||||||
|  |                         false | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } else { | ||||||
|  |                 false | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Will call [SmartSemaphore.Mutable.lock], then execute [action] and return the result after [SmartSemaphore.Mutable.unlock] | ||||||
|  |  */ | ||||||
|  | @OptIn(ExperimentalContracts::class) | ||||||
|  | suspend inline fun <T> SmartSemaphore.Mutable.withAcquire(permits: Int = 1, action: () -> T): T { | ||||||
|  |     contract { | ||||||
|  |         callsInPlace(action, InvocationKind.EXACTLY_ONCE) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     acquire(permits) | ||||||
|  |     try { | ||||||
|  |         return action() | ||||||
|  |     } finally { | ||||||
|  |         release(permits) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Will wait until the [SmartSemaphore.permitsStateFlow] of [this] instance will have [permits] count free permits. | ||||||
|  |  * | ||||||
|  |  * Anyway, after the end of this block there are no any guaranties that [SmartSemaphore.freePermits] >= [permits] due to | ||||||
|  |  * the fact that some other parties may lock it again | ||||||
|  |  */ | ||||||
|  | suspend fun SmartSemaphore.waitRelease(permits: Int = 1) = permitsStateFlow.first { it >= permits } | ||||||
| @@ -0,0 +1,86 @@ | |||||||
|  | package dev.inmo.micro_utils.coroutines | ||||||
|  |  | ||||||
|  | import kotlinx.coroutines.CoroutineScope | ||||||
|  | import kotlinx.coroutines.Dispatchers | ||||||
|  | import kotlinx.coroutines.ExperimentalCoroutinesApi | ||||||
|  | import kotlinx.coroutines.InternalCoroutinesApi | ||||||
|  | import kotlinx.coroutines.channels.BufferOverflow | ||||||
|  | import kotlinx.coroutines.flow.FlowCollector | ||||||
|  | import kotlinx.coroutines.flow.MutableSharedFlow | ||||||
|  | import kotlinx.coroutines.flow.MutableStateFlow | ||||||
|  | import kotlinx.coroutines.flow.StateFlow | ||||||
|  | import kotlinx.coroutines.internal.SynchronizedObject | ||||||
|  | import kotlinx.coroutines.internal.synchronized | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Works like [StateFlow], but guarantee that latest value update will always be delivered to | ||||||
|  |  * each active subscriber | ||||||
|  |  */ | ||||||
|  | open class SpecialMutableStateFlow<T>( | ||||||
|  |     initialValue: T, | ||||||
|  |     internalScope: CoroutineScope = CoroutineScope(Dispatchers.Default) | ||||||
|  | ) : MutableStateFlow<T>, FlowCollector<T>, MutableSharedFlow<T> { | ||||||
|  |     @OptIn(InternalCoroutinesApi::class) | ||||||
|  |     private val syncObject = SynchronizedObject() | ||||||
|  |     protected val internalSharedFlow: MutableSharedFlow<T> = MutableSharedFlow( | ||||||
|  |         replay = 0, | ||||||
|  |         extraBufferCapacity = 2, | ||||||
|  |         onBufferOverflow = BufferOverflow.DROP_OLDEST | ||||||
|  |     ) | ||||||
|  |     protected val publicSharedFlow: MutableSharedFlow<T> = MutableSharedFlow( | ||||||
|  |         replay = 1, | ||||||
|  |         extraBufferCapacity = 1, | ||||||
|  |         onBufferOverflow = BufferOverflow.DROP_OLDEST | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     protected var _value: T = initialValue | ||||||
|  |     override var value: T | ||||||
|  |         get() = _value | ||||||
|  |         set(value) { | ||||||
|  |             doOnChangeAction(value) | ||||||
|  |         } | ||||||
|  |     protected val job = internalSharedFlow.subscribe(internalScope) { | ||||||
|  |         doOnChangeAction(it) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override val replayCache: List<T> | ||||||
|  |         get() = publicSharedFlow.replayCache | ||||||
|  |     override val subscriptionCount: StateFlow<Int> | ||||||
|  |         get() = publicSharedFlow.subscriptionCount | ||||||
|  |  | ||||||
|  |     @OptIn(InternalCoroutinesApi::class) | ||||||
|  |     override fun compareAndSet(expect: T, update: T): Boolean { | ||||||
|  |         return synchronized(syncObject) { | ||||||
|  |             if (expect == _value && update != _value) { | ||||||
|  |                 doOnChangeAction(update) | ||||||
|  |             } | ||||||
|  |             expect == _value | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     protected open fun onChangeWithoutSync(value: T) { | ||||||
|  |         _value = value | ||||||
|  |         publicSharedFlow.tryEmit(value) | ||||||
|  |     } | ||||||
|  |     @OptIn(InternalCoroutinesApi::class) | ||||||
|  |     protected open fun doOnChangeAction(value: T) { | ||||||
|  |         synchronized(syncObject) { | ||||||
|  |             if (_value != value) { | ||||||
|  |                 onChangeWithoutSync(value) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @ExperimentalCoroutinesApi | ||||||
|  |     override fun resetReplayCache() = publicSharedFlow.resetReplayCache() | ||||||
|  |  | ||||||
|  |     override fun tryEmit(value: T): Boolean { | ||||||
|  |         return internalSharedFlow.tryEmit(value) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override suspend fun emit(value: T) { | ||||||
|  |         internalSharedFlow.emit(value) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override suspend fun collect(collector: FlowCollector<T>) = publicSharedFlow.collect(collector) | ||||||
|  | } | ||||||
							
								
								
									
										151
									
								
								coroutines/src/commonTest/kotlin/SmartRWLockerTests.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										151
									
								
								coroutines/src/commonTest/kotlin/SmartRWLockerTests.kt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,151 @@ | |||||||
|  | import dev.inmo.micro_utils.coroutines.* | ||||||
|  | import kotlinx.coroutines.* | ||||||
|  | import kotlinx.coroutines.flow.first | ||||||
|  | import kotlinx.coroutines.sync.Mutex | ||||||
|  | import kotlinx.coroutines.sync.withLock | ||||||
|  | import kotlinx.coroutines.test.runTest | ||||||
|  | import kotlin.test.Test | ||||||
|  | import kotlin.test.assertEquals | ||||||
|  | import kotlin.test.assertTrue | ||||||
|  |  | ||||||
|  | class SmartRWLockerTests { | ||||||
|  |     @Test | ||||||
|  |     fun compositeTest() { | ||||||
|  |         val locker = SmartRWLocker() | ||||||
|  |  | ||||||
|  |         val readAndWriteWorkers = 10 | ||||||
|  |         runTest { | ||||||
|  |             var started = 0 | ||||||
|  |             var done = 0 | ||||||
|  |             val doneMutex = Mutex() | ||||||
|  |             val readWorkers = (0 until readAndWriteWorkers).map { | ||||||
|  |                 launch(start = CoroutineStart.LAZY) { | ||||||
|  |                     locker.withReadAcquire { | ||||||
|  |                         doneMutex.withLock { | ||||||
|  |                             started++ | ||||||
|  |                         } | ||||||
|  |                         delay(100L) | ||||||
|  |                         doneMutex.withLock { | ||||||
|  |                             done++ | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             var doneWrites = 0 | ||||||
|  |  | ||||||
|  |             val writeWorkers = (0 until readAndWriteWorkers).map { | ||||||
|  |                 launch(start = CoroutineStart.LAZY) { | ||||||
|  |                     locker.withWriteLock { | ||||||
|  |                         assertTrue(done == readAndWriteWorkers || started == 0) | ||||||
|  |                         delay(10L) | ||||||
|  |                         doneWrites++ | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             readWorkers.forEach { it.start() } | ||||||
|  |             writeWorkers.forEach { it.start() } | ||||||
|  |  | ||||||
|  |             readWorkers.joinAll() | ||||||
|  |             writeWorkers.joinAll() | ||||||
|  |  | ||||||
|  |             assertEquals(expected = readAndWriteWorkers, actual = done) | ||||||
|  |             assertEquals(expected = readAndWriteWorkers, actual = doneWrites) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Test | ||||||
|  |     fun simpleWithWriteLockTest() { | ||||||
|  |         val locker = SmartRWLocker() | ||||||
|  |  | ||||||
|  |         runTest { | ||||||
|  |             locker.withWriteLock { | ||||||
|  |                 assertEquals(0, locker.readSemaphore.freePermits) | ||||||
|  |                 assertEquals(true, locker.writeMutex.isLocked) | ||||||
|  |             } | ||||||
|  |             assertEquals(Int.MAX_VALUE, locker.readSemaphore.freePermits) | ||||||
|  |             assertEquals(false, locker.writeMutex.isLocked) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Test | ||||||
|  |     fun failureWithWriteLockTest() { | ||||||
|  |         val locker = SmartRWLocker() | ||||||
|  |  | ||||||
|  |         val exception = IllegalArgumentException() | ||||||
|  |         try { | ||||||
|  |             runTest { | ||||||
|  |                 val subscope = kotlinx.coroutines.CoroutineScope(this.coroutineContext) | ||||||
|  |                 var happenException: Throwable? = null | ||||||
|  |                 try { | ||||||
|  |                     locker.withWriteLock { | ||||||
|  |                         val checkFunction = fun (): Deferred<Unit> { | ||||||
|  |                             return subscope.async { | ||||||
|  |                                 assertEquals(0, locker.readSemaphore.freePermits) | ||||||
|  |                                 assertEquals(true, locker.writeMutex.isLocked) | ||||||
|  |                                 throw exception | ||||||
|  |                             } | ||||||
|  |                         } | ||||||
|  |                         doInDefault { | ||||||
|  |                             assertEquals(0, locker.readSemaphore.freePermits) | ||||||
|  |                             assertEquals(true, locker.writeMutex.isLocked) | ||||||
|  |                             checkFunction().await() | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } catch (e: Exception) { | ||||||
|  |                     happenException = e | ||||||
|  |                 } | ||||||
|  |                 if (exception != happenException) { | ||||||
|  |                     assertEquals(exception, happenException ?.cause) | ||||||
|  |                 } | ||||||
|  |                 assertEquals(Int.MAX_VALUE, locker.readSemaphore.freePermits) | ||||||
|  |                 assertEquals(false, locker.writeMutex.isLocked) | ||||||
|  |             } | ||||||
|  |         } catch (e: Exception) { | ||||||
|  |             assertEquals(exception, e) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Test | ||||||
|  |     fun simpleWithReadAcquireTest() { | ||||||
|  |         val locker = SmartRWLocker() | ||||||
|  |  | ||||||
|  |         runTest { | ||||||
|  |             locker.withReadAcquire { | ||||||
|  |                 assertEquals(Int.MAX_VALUE - 1, locker.readSemaphore.freePermits) | ||||||
|  |                 assertEquals(false, locker.writeMutex.isLocked) | ||||||
|  |                 locker.withReadAcquire { | ||||||
|  |                     assertEquals(Int.MAX_VALUE - 2, locker.readSemaphore.freePermits) | ||||||
|  |                     assertEquals(false, locker.writeMutex.isLocked) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             assertEquals(Int.MAX_VALUE, locker.readSemaphore.freePermits) | ||||||
|  |             assertEquals(false, locker.writeMutex.isLocked) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Test | ||||||
|  |     fun simple2WithWriteLockTest() { | ||||||
|  |         val locker = SmartRWLocker() | ||||||
|  |  | ||||||
|  |         val unlockDelay = 1000L // 1 sec | ||||||
|  |         var unlocked: Boolean = false | ||||||
|  |         runTest { | ||||||
|  |             launch { | ||||||
|  |                 locker.withReadAcquire { | ||||||
|  |                     delay(unlockDelay) | ||||||
|  |                 } | ||||||
|  |                 unlocked = true | ||||||
|  |             } | ||||||
|  |             locker.readSemaphore.permitsStateFlow.first { it == Int.MAX_VALUE - 1 } | ||||||
|  |             assertEquals(false, unlocked) | ||||||
|  |             locker.withWriteLock { | ||||||
|  |                 assertEquals(true, unlocked) | ||||||
|  |                 assertEquals(0, locker.readSemaphore.freePermits) | ||||||
|  |                 assertEquals(true, locker.writeMutex.isLocked) | ||||||
|  |             } | ||||||
|  |             assertEquals(Int.MAX_VALUE, locker.readSemaphore.freePermits) | ||||||
|  |             assertEquals(false, locker.writeMutex.isLocked) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -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 | ||||||
|  | } | ||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user