mirror of
				https://github.com/InsanusMokrassar/MicroUtils.git
				synced 2025-10-30 03:30:19 +00:00 
			
		
		
		
	Compare commits
	
		
			819 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | |||
| 00cc26d874 | |||
| 02520636ad | |||
| 76813fae8e | |||
| 4693483c2b | |||
| 6dbd12df59 | |||
| 9e84dc5031 | |||
| 8f790360bc | |||
| 808375cea6 | |||
| d9c15db9de | |||
| cf2be8ed43 | |||
| 97339c9b1d | |||
| eb35903de9 | |||
| 43e88e00da | |||
| 1e51f9d77b | |||
| dad738357c | |||
| cb828ab3f2 | |||
| aefaf4a8cc | |||
| b29f37a251 | |||
| 88854020ac | |||
| 14ebe01fc6 | |||
| 771aed0f0f | |||
| 16c57fcd6a | |||
| 75397a7ccb | |||
| b05404f828 | |||
| edea942874 | |||
| 8a8f568b9a | |||
| 4dc8d30c52 | |||
| cb4e08e823 | |||
| c443bf4fa0 | |||
| a6982de822 | |||
| 4f1a663e75 | |||
| dab262d626 | |||
| 87a3f61ca6 | |||
| 506e937a68 | |||
| 5a037c76dd | |||
| 313f622f7e | |||
| 6cba1fe1a2 | |||
| fd2d0e80b7 | |||
| 96ab2e8aca | |||
| 0202988cae | |||
| d619d59947 | |||
| 85b3e48d18 | |||
| 7a9b7d98a1 | |||
| b212acfcaf | |||
| 3a45e5dc70 | |||
| 73190518d5 | |||
| 03f78180dc | |||
| 1c0b8cf842 | |||
| a1624ea2a9 | |||
| 23a050cf1e | |||
| 916f2f96f4 | |||
| 00cc214754 | |||
| b2e38f72b9 | |||
| e7107d238d | |||
| ed9ebdbd1a | |||
| e80676d3d2 | |||
| 02d02fa8f2 | |||
| bd783fb74f | |||
| 50386adf70 | |||
| f4ee6c2890 | |||
| d45aef9fe5 | |||
| a56cd3dddd | |||
| 419e7070ee | |||
| 612cf40b5f | |||
| 8b39882e83 | |||
| e639ae172b | |||
| d0446850ae | |||
| c48465b90b | |||
| f419fd03d2 | |||
| 494812a660 | |||
| eb78f21eec | |||
| 4bda70268b | |||
| f037ce4371 | |||
| 3d2196e35d | |||
| a74f061b02 | |||
| 11ade14676 | |||
| eb562d8784 | |||
| 1ee5b4bfd4 | |||
| d97892080b | |||
| 6f37125724 | |||
| ed1baaade7 | |||
| bb9669f8fd | |||
| bdac715d48 | |||
| acf4971298 | |||
| 249bc83a8c | |||
| 0fbb92f03f | |||
| ca27cb3f82 | |||
| 3a5771a0cc | |||
| 527a2a91ac | |||
| 6763e5c4c6 | |||
| 06918d8310 | |||
| 89ccaa1b57 | |||
| 5d0bdb9bcf | |||
| 31fdcf74a5 | |||
| afca09cc1d | |||
| 531d89d9db | |||
| 6bbbea0bc3 | |||
| e337cd98c8 | |||
| bcbab3b380 | |||
| fb63de7568 | |||
| aa45a4ab13 | |||
| 2af7e2f681 | |||
| 34fd9edce0 | |||
| 2a4cb8c5f9 | |||
| 50ea40bc3a | |||
| a77654052d | |||
| 88aafce552 | |||
| 4e95d6bfff | |||
| 38d0e34fb5 | |||
| 8fbc6b9041 | |||
| e8219d6cf4 | |||
| 6c20fc4ca6 | |||
| 85cd975492 | |||
| 1171a717fe | |||
| bbe5320312 | |||
| 00acb9fddd | |||
| de3d14dc41 | |||
| 67ff9cc9b3 | |||
| af132103a0 | |||
| 3b1124a804 | |||
| f226c2dfd6 | |||
| 69d6e63846 | |||
| 02c3d397ad | |||
| 67a1050646 | |||
| 8cd0775a6c | |||
| 162294d6c6 | |||
| c4dd19dd00 | |||
| d2314422f1 | |||
| 6fedd6f859 | |||
| e52b59665f | |||
| cda9d09689 | |||
| c9237b3f00 | |||
| 18bba66c4a | |||
| 63418c4a8a | |||
| 2e66c6f4e3 | |||
| e9c5df4c13 | |||
| bc7789ad2c | |||
| e3da761249 | |||
| 4082f65afa | |||
| 5d1cab075d | |||
| bcf67f7e59 | |||
| 7d3b1f8e75 | |||
| 119a0588cc | |||
| fab789d9c0 | |||
| ceba81c08f | |||
| a061af0558 | |||
| c7a53846ad | |||
| a683cccf0c | |||
| 50d41e35c1 | |||
| aa0e831cea | |||
| 44e26ccb4f | |||
| 2a783f6e2b | |||
| 6058d6a724 | |||
| 2e9c7eb5fa | |||
| e75465ad10 | |||
| de01ad54e9 | |||
| eeea7ddbe3 | |||
| e0b18bec05 | |||
| 410e89bba9 | |||
| 9ef19dc42b | |||
| 0337d1b82d | |||
| f5bd4c5ccb | |||
| 630f9bc0d4 | |||
| 18b4ffece1 | |||
| f64e1effa3 | |||
| 847fcbb488 | |||
| 88002ec8e7 | |||
| 7f8db6a29d | |||
| b183b82443 | |||
| 5dad27de72 | 
							
								
								
									
										16
									
								
								.github/workflows/build.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										16
									
								
								.github/workflows/build.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,4 +1,5 @@ | |||||||
| name: Regular build |  | ||||||
|  | name: Build | ||||||
| on: [push] | on: [push] | ||||||
| jobs: | jobs: | ||||||
|   build: |   build: | ||||||
| @@ -7,6 +8,17 @@ jobs: | |||||||
|       - uses: actions/checkout@v2 |       - uses: actions/checkout@v2 | ||||||
|       - uses: actions/setup-java@v1 |       - uses: actions/setup-java@v1 | ||||||
|         with: |         with: | ||||||
|           java-version: 1.8 |           java-version: 11 | ||||||
|  |       - name: Rewrite version | ||||||
|  |         run: | | ||||||
|  |           branch="`echo "${{ github.ref }}" | grep -o "[^/]*$"`" | ||||||
|  |           cat gradle.properties | sed -e "s/^version=\([0-9\.]*\)/version=\1-branch_$branch-build${{ github.run_number }}/" > gradle.properties.tmp | ||||||
|  |           rm gradle.properties | ||||||
|  |           mv gradle.properties.tmp gradle.properties | ||||||
|       - name: Build |       - name: Build | ||||||
|         run: ./gradlew build |         run: ./gradlew build | ||||||
|  |       - name: Publish | ||||||
|  |         continue-on-error: true | ||||||
|  |         run: ./gradlew publishAllPublicationsToGiteaRepository | ||||||
|  |         env: | ||||||
|  |           GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} | ||||||
|   | |||||||
							
								
								
									
										4
									
								
								.github/workflows/dokka_push.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/dokka_push.yml
									
									
									
									
										vendored
									
									
								
							| @@ -10,9 +10,9 @@ jobs: | |||||||
|       - uses: actions/checkout@v2 |       - uses: actions/checkout@v2 | ||||||
|       - uses: actions/setup-java@v1 |       - uses: actions/setup-java@v1 | ||||||
|         with: |         with: | ||||||
|           java-version: 1.8 |           java-version: 11 | ||||||
|       - 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: | ||||||
|   | |||||||
							
								
								
									
										24
									
								
								.github/workflows/packages_push.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										24
									
								
								.github/workflows/packages_push.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,24 +0,0 @@ | |||||||
|  |  | ||||||
| name: Publish package to GitHub Packages |  | ||||||
| on: [push] |  | ||||||
| jobs: |  | ||||||
|   publishing: |  | ||||||
|     runs-on: ubuntu-latest |  | ||||||
|     steps: |  | ||||||
|       - uses: actions/checkout@v2 |  | ||||||
|       - uses: actions/setup-java@v1 |  | ||||||
|         with: |  | ||||||
|           java-version: 1.8 |  | ||||||
|       - name: Rewrite version |  | ||||||
|         run: | |  | ||||||
|           branch="`echo "${{ github.ref }}" | grep -o "[^/]*$"`" |  | ||||||
|           cat gradle.properties | sed -e "s/^version=\([0-9\.]*\)/version=\1-branch_$branch-build${{ github.run_number }}/" > gradle.properties.tmp |  | ||||||
|           rm gradle.properties |  | ||||||
|           mv gradle.properties.tmp gradle.properties |  | ||||||
|       - name: Build |  | ||||||
|         run: ./gradlew build |  | ||||||
|       - name: Publish |  | ||||||
|         run: ./gradlew --no-parallel publishAllPublicationsToGithubPackagesRepository -x signJsPublication -x signJvmPublication -x signKotlinMultiplatformPublication -x signAndroidDebugPublication -x signAndroidReleasePublication -x signKotlinMultiplatformPublication |  | ||||||
|         env: |  | ||||||
|           GITHUBPACKAGES_USER: ${{ github.actor }} |  | ||||||
|           GITHUBPACKAGES_PASSWORD: ${{ secrets.GITHUB_TOKEN }} |  | ||||||
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -11,5 +11,6 @@ out/ | |||||||
|  |  | ||||||
| secret.gradle | secret.gradle | ||||||
| local.properties | local.properties | ||||||
|  | kotlin-js-store | ||||||
|  |  | ||||||
| publishing.sh | publishing.sh | ||||||
|   | |||||||
| @@ -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") |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
							
								
								
									
										1146
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										1146
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -10,7 +10,7 @@ kotlin { | |||||||
|     sourceSets { |     sourceSets { | ||||||
|         androidMain { |         androidMain { | ||||||
|             dependencies { |             dependencies { | ||||||
|                 api "androidx.appcompat:appcompat-resources:$appcompat_version" |                 api libs.android.appCompat.resources | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|   | |||||||
							
								
								
									
										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 package="dev.inmo.micro_utils.android.pickers"/> | ||||||
							
								
								
									
										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) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -10,13 +10,13 @@ kotlin { | |||||||
|     sourceSets { |     sourceSets { | ||||||
|         commonMain { |         commonMain { | ||||||
|             dependencies { |             dependencies { | ||||||
|                 api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version" |                 api libs.kt.coroutines | ||||||
|                 api project(":micro_utils.common") |                 api project(":micro_utils.common") | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         androidMain { |         androidMain { | ||||||
|             dependencies { |             dependencies { | ||||||
|                 api "androidx.recyclerview:recyclerview:$androidx_recycler_version" |                 api libs.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 package="dev.inmo.micro_utils.android.smalltextfield"/> | ||||||
| @@ -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 | ||||||
|  |             } | ||||||
|  |         ) | ||||||
|  |     ) | ||||||
|  | } | ||||||
							
								
								
									
										20
									
								
								build.gradle
									
									
									
									
									
								
							
							
						
						
									
										20
									
								
								build.gradle
									
									
									
									
									
								
							| @@ -7,20 +7,27 @@ buildscript { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     dependencies { |     dependencies { | ||||||
|         classpath 'com.android.tools.build:gradle:4.1.3' |         classpath libs.buildscript.kt.gradle | ||||||
|         classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" |         classpath libs.buildscript.kt.serialization | ||||||
|         classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" |         classpath libs.buildscript.kt.ksp | ||||||
|         classpath "com.getkeepsafe.dexcount:dexcount-gradle-plugin:$dexcount_version" |         classpath libs.buildscript.jb.dokka | ||||||
|         classpath "com.github.breadmoirai:github-release:$github_release_plugin_version" |         classpath libs.buildscript.gh.release | ||||||
|         classpath "org.jetbrains.dokka:dokka-gradle-plugin:$dokka_version" |         classpath libs.buildscript.android.gradle | ||||||
|  |         classpath libs.buildscript.android.dexcount | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | 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://git.inmo.dev/api/packages/InsanusMokrassar/maven" } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     // 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" | ||||||
|   | |||||||
| @@ -16,6 +16,19 @@ 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 | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|   | |||||||
							
								
								
									
										18
									
								
								common/compose/build.gradle
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								common/compose/build.gradle
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | |||||||
|  | plugins { | ||||||
|  |     id "org.jetbrains.kotlin.multiplatform" | ||||||
|  |     id "org.jetbrains.kotlin.plugin.serialization" | ||||||
|  |     id "com.android.library" | ||||||
|  |     alias(libs.plugins.jb.compose) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | apply from: "$mppProjectWithSerializationAndComposePresetPath" | ||||||
|  |  | ||||||
|  | kotlin { | ||||||
|  |     sourceSets { | ||||||
|  |         commonMain { | ||||||
|  |             dependencies { | ||||||
|  |                 api project(":micro_utils.common") | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										1
									
								
								common/compose/src/androidMain/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								common/compose/src/androidMain/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | <manifest package="dev.inmo.micro_utils.common.compose"/> | ||||||
| @@ -0,0 +1,16 @@ | |||||||
|  | package dev.inmo.micro_utils.common.compose | ||||||
|  |  | ||||||
|  | import androidx.compose.runtime.DisposableEffectResult | ||||||
|  |  | ||||||
|  | class DefaultDisposableEffectResult( | ||||||
|  |     private val onDispose: () -> Unit | ||||||
|  | ) : DisposableEffectResult { | ||||||
|  |     override fun dispose() { | ||||||
|  |         onDispose() | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     companion object { | ||||||
|  |         val DoNothing = DefaultDisposableEffectResult {} | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
| @@ -0,0 +1,10 @@ | |||||||
|  | package dev.inmo.micro_utils.common.compose | ||||||
|  |  | ||||||
|  | import androidx.compose.runtime.MutableState | ||||||
|  | import androidx.compose.runtime.State | ||||||
|  | import androidx.compose.runtime.derivedStateOf | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Converts current [MutableState] to immutable [State] using [derivedStateOf] | ||||||
|  |  */ | ||||||
|  | fun <T> MutableState<T>.asState(): State<T> = derivedStateOf { this.value } | ||||||
| @@ -0,0 +1,9 @@ | |||||||
|  | package dev.inmo.micro_utils.common.compose | ||||||
|  |  | ||||||
|  | import androidx.compose.runtime.Composition | ||||||
|  | import dev.inmo.micro_utils.common.onRemoved | ||||||
|  | import org.w3c.dom.Element | ||||||
|  |  | ||||||
|  | fun Composition.linkWithElement(element: Element) { | ||||||
|  |     element.onRemoved { dispose() } | ||||||
|  | } | ||||||
| @@ -0,0 +1,10 @@ | |||||||
|  | package dev.inmo.micro_utils.common.compose | ||||||
|  |  | ||||||
|  | import org.jetbrains.compose.web.attributes.ATarget | ||||||
|  |  | ||||||
|  | fun openLink(link: String, mode: ATarget = ATarget.Blank, features: String = "") = dev.inmo.micro_utils.common.openLink( | ||||||
|  |     link, | ||||||
|  |     mode.targetStr, | ||||||
|  |     features | ||||||
|  | ) | ||||||
|  |  | ||||||
| @@ -0,0 +1,13 @@ | |||||||
|  | package dev.inmo.micro_utils.common.compose | ||||||
|  |  | ||||||
|  | import androidx.compose.runtime.* | ||||||
|  | import org.jetbrains.compose.web.dom.DOMScope | ||||||
|  | import org.w3c.dom.Element | ||||||
|  |  | ||||||
|  | fun <TElement : Element> renderComposableAndLinkToRoot( | ||||||
|  |     root: TElement, | ||||||
|  |     monotonicFrameClock: MonotonicFrameClock = DefaultMonotonicFrameClock, | ||||||
|  |     content: @Composable DOMScope<TElement>.() -> Unit | ||||||
|  | ): Composition = org.jetbrains.compose.web.renderComposable(root, monotonicFrameClock, content).apply { | ||||||
|  |     linkWithElement(root) | ||||||
|  | } | ||||||
| @@ -0,0 +1,43 @@ | |||||||
|  | package dev.inmo.micro_utils.common.compose | ||||||
|  |  | ||||||
|  | import org.jetbrains.compose.web.css.* | ||||||
|  |  | ||||||
|  | object SkeletonAnimation : StyleSheet() { | ||||||
|  |     val skeletonKeyFrames: CSSNamedKeyframes by keyframes { | ||||||
|  |         to { backgroundPosition("-20% 0") } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fun CSSBuilder.includeSkeletonStyle( | ||||||
|  |         duration: CSSSizeValue<out CSSUnitTime> = 2.s, | ||||||
|  |         timingFunction: AnimationTimingFunction = AnimationTimingFunction.EaseInOut, | ||||||
|  |         iterationCount: Int? = null, | ||||||
|  |         direction: AnimationDirection = AnimationDirection.Normal, | ||||||
|  |         keyFrames: CSSNamedKeyframes = skeletonKeyFrames, | ||||||
|  |         hideChildren: Boolean = true, | ||||||
|  |         hideText: Boolean = hideChildren | ||||||
|  |     ) { | ||||||
|  |         backgroundImage("linear-gradient(110deg, rgb(236, 236, 236) 40%, rgb(245, 245, 245) 50%, rgb(236, 236, 236) 65%)") | ||||||
|  |         backgroundSize("200% 100%") | ||||||
|  |         backgroundPosition("180% 0") | ||||||
|  |         animation(keyFrames) { | ||||||
|  |             duration(duration) | ||||||
|  |             timingFunction(timingFunction) | ||||||
|  |             iterationCount(iterationCount) | ||||||
|  |             direction(direction) | ||||||
|  |         } | ||||||
|  |         if (hideText) { | ||||||
|  |             property("color", "${Color.transparent} !important") | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (hideChildren) { | ||||||
|  |             child(self, universal) style { | ||||||
|  |                 property("visibility", "hidden") | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     val skeleton by style { | ||||||
|  |         includeSkeletonStyle() | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
| @@ -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( | ||||||
|   | |||||||
| @@ -1,5 +0,0 @@ | |||||||
| package dev.inmo.micro_utils.common |  | ||||||
|  |  | ||||||
| @Deprecated("Redundant", ReplaceWith("coerceIn(min, max)")) |  | ||||||
| @Suppress("NOTHING_TO_INLINE") |  | ||||||
| inline fun <T : Comparable<T>> T.clamp(min: T, max: T): T = coerceIn(min, max) |  | ||||||
| @@ -2,7 +2,7 @@ | |||||||
|  |  | ||||||
| package dev.inmo.micro_utils.common | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
| import kotlin.jvm.JvmInline | import kotlinx.serialization.Serializable | ||||||
|  |  | ||||||
| private inline fun <T> getObject( | private inline fun <T> getObject( | ||||||
|     additional: MutableList<T>, |     additional: MutableList<T>, | ||||||
| @@ -16,16 +16,29 @@ private inline fun <T> getObject( | |||||||
| /** | /** | ||||||
|  * Diff object which contains information about differences between two [Iterable]s |  * Diff object which contains information about differences between two [Iterable]s | ||||||
|  * |  * | ||||||
|  |  * See tests for more info | ||||||
|  |  * | ||||||
|  |  * @param removed The objects which has been presented in the old collection but absent in new one. Index here is the index in the old collection | ||||||
|  |  * @param added The object which appear in new collection only. Indexes here show the index in the new collection | ||||||
|  |  * @param replaced Pair of old-new changes. First object has been presented in the old collection on its | ||||||
|  |  * [IndexedValue.index] place, the second one is the object in new collection. Both have indexes due to the fact that in | ||||||
|  |  * case when some value has been replaced after adds or removes in original collection the object index will be changed | ||||||
|  |  * | ||||||
|  * @see calculateDiff |  * @see calculateDiff | ||||||
|  */ |  */ | ||||||
|  | @Serializable | ||||||
| data class Diff<T> internal constructor( | data class Diff<T> internal constructor( | ||||||
|     val removed: List<IndexedValue<T>>, |     val removed: List<@Serializable(IndexedValueSerializer::class) IndexedValue<T>>, | ||||||
|     /** |     /** | ||||||
|      * Old-New values pairs |      * Old-New values pairs | ||||||
|      */ |      */ | ||||||
|     val replaced: List<Pair<IndexedValue<T>, IndexedValue<T>>>, |     val replaced: List<Pair<@Serializable(IndexedValueSerializer::class) IndexedValue<T>, @Serializable(IndexedValueSerializer::class) IndexedValue<T>>>, | ||||||
|     val added: List<IndexedValue<T>> |     val added: List<@Serializable(IndexedValueSerializer::class) IndexedValue<T>> | ||||||
| ) | ) { | ||||||
|  |     fun isEmpty(): Boolean = removed.isEmpty() && replaced.isEmpty() && added.isEmpty() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fun <T> emptyDiff(): Diff<T> = Diff(emptyList(), emptyList(), emptyList()) | ||||||
|  |  | ||||||
| private inline fun <T> performChanges( | private inline fun <T> performChanges( | ||||||
|     potentialChanges: MutableList<Pair<IndexedValue<T>?, IndexedValue<T>?>>, |     potentialChanges: MutableList<Pair<IndexedValue<T>?, IndexedValue<T>?>>, | ||||||
| @@ -34,17 +47,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 | ||||||
|                 } |                 } | ||||||
|             ) |             ) | ||||||
| @@ -94,7 +108,7 @@ private inline fun <T> performChanges( | |||||||
|  */ |  */ | ||||||
| fun <T> Iterable<T>.calculateDiff( | fun <T> Iterable<T>.calculateDiff( | ||||||
|     other: Iterable<T>, |     other: Iterable<T>, | ||||||
|     strictComparison: Boolean = false |     comparisonFun: (T?, T?) -> Boolean | ||||||
| ): Diff<T> { | ): Diff<T> { | ||||||
|     var i = -1 |     var i = -1 | ||||||
|     var j = -1 |     var j = -1 | ||||||
| @@ -122,31 +136,60 @@ fun <T> Iterable<T>.calculateDiff( | |||||||
|         } |         } | ||||||
|  |  | ||||||
|         when { |         when { | ||||||
|             oldObject === newObject || (oldObject == newObject && !strictComparison) -> { |             comparisonFun(oldObject, newObject) -> { | ||||||
|                 changedObjects.addAll(potentiallyChangedObjects.map { it as Pair<IndexedValue<T>, IndexedValue<T>> }) |                 changedObjects.addAll(potentiallyChangedObjects.map { | ||||||
|  |                     @Suppress("UNCHECKED_CAST") | ||||||
|  |                     it as Pair<IndexedValue<T>, IndexedValue<T>> | ||||||
|  |                 }) | ||||||
|                 potentiallyChangedObjects.clear() |                 potentiallyChangedObjects.clear() | ||||||
|             } |             } | ||||||
|             else -> { |             else -> { | ||||||
|                 potentiallyChangedObjects.add(oldObject ?.let { IndexedValue(i, oldObject) } to newObject ?.let { IndexedValue(j, newObject) }) |                 potentiallyChangedObjects.add(oldObject ?.let { IndexedValue(i, oldObject) } to newObject ?.let { IndexedValue(j, newObject) }) | ||||||
|                 val previousOldsAdditionsSize = additionalInOld.size |                 val previousOldsAdditionsSize = additionalInOld.size | ||||||
|                 val previousNewsAdditionsSize = additionalInNew.size |                 val previousNewsAdditionsSize = additionalInNew.size | ||||||
|                 performChanges(potentiallyChangedObjects, additionalInOld, additionalInNew, changedObjects, removedObjects, addedObjects, strictComparison) |                 performChanges(potentiallyChangedObjects, additionalInOld, additionalInNew, changedObjects, removedObjects, addedObjects, comparisonFun) | ||||||
|                 i -= (additionalInOld.size - previousOldsAdditionsSize) |                 i -= (additionalInOld.size - previousOldsAdditionsSize) | ||||||
|                 j -= (additionalInNew.size - previousNewsAdditionsSize) |                 j -= (additionalInNew.size - previousNewsAdditionsSize) | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|     potentiallyChangedObjects.add(null to null) |     potentiallyChangedObjects.add(null to null) | ||||||
|     performChanges(potentiallyChangedObjects, additionalInOld, additionalInNew, changedObjects, removedObjects, addedObjects, strictComparison) |     performChanges(potentiallyChangedObjects, additionalInOld, additionalInNew, changedObjects, removedObjects, addedObjects, comparisonFun) | ||||||
|  |  | ||||||
|     return Diff(removedObjects.toList(), changedObjects.toList(), addedObjects.toList()) |     return Diff(removedObjects.toList(), changedObjects.toList(), addedObjects.toList()) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 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) | ||||||
|  |  | ||||||
| /** | /** | ||||||
| @@ -155,3 +198,52 @@ inline fun <T> StrictDiff(old: Iterable<T>, new: Iterable<T>) = old.calculateDif | |||||||
| inline fun <T> Iterable<T>.calculateStrictDiff( | inline fun <T> Iterable<T>.calculateStrictDiff( | ||||||
|     other: Iterable<T> |     other: Iterable<T> | ||||||
| ) = calculateDiff(other, strictComparison = true) | ) = calculateDiff(other, strictComparison = true) | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Applies [diff] to [this] [MutableList] | ||||||
|  |  */ | ||||||
|  | fun <T> MutableList<T>.applyDiff( | ||||||
|  |     diff: Diff<T> | ||||||
|  | ) { | ||||||
|  |     for (i in diff.removed.indices.sortedDescending()) { | ||||||
|  |         removeAt(diff.removed[i].index) | ||||||
|  |     } | ||||||
|  |     diff.added.forEach { (i, t) -> | ||||||
|  |         add(i, t) | ||||||
|  |     } | ||||||
|  |     diff.replaced.forEach { (_, new) -> | ||||||
|  |         set(new.index, new.value) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * This method call [calculateDiff] with strict mode [strictComparison] and then apply differences to [this] | ||||||
|  |  * mutable list | ||||||
|  |  */ | ||||||
|  | fun <T> MutableList<T>.applyDiff( | ||||||
|  |     source: Iterable<T>, | ||||||
|  |     strictComparison: Boolean = false | ||||||
|  | ): Diff<T> = calculateDiff(source, strictComparison).also { | ||||||
|  |     applyDiff(it) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * This method call [calculateDiff] and then apply differences to [this] | ||||||
|  |  * mutable list | ||||||
|  |  */ | ||||||
|  | fun <T> MutableList<T>.applyDiff( | ||||||
|  |     source: Iterable<T>, | ||||||
|  |     comparisonFun: (T?, T?) -> Boolean | ||||||
|  | ): Diff<T> = calculateDiff(source, comparisonFun).also { | ||||||
|  |     applyDiff(it) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Reverse [this] [Diff]. Result will contain [Diff.added] on [Diff.removed] (and vice-verse), all the | ||||||
|  |  * [Diff.replaced] values will be reversed too | ||||||
|  |  */ | ||||||
|  | fun <T> Diff<T>.reversed() = Diff( | ||||||
|  |     removed = added, | ||||||
|  |     replaced = replaced.map { it.second to it.first }, | ||||||
|  |     added = removed | ||||||
|  | ) | ||||||
|   | |||||||
| @@ -0,0 +1,160 @@ | |||||||
|  | @file:Suppress("unused", "NOTHING_TO_INLINE") | ||||||
|  |  | ||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | import kotlinx.serialization.* | ||||||
|  | import kotlinx.serialization.builtins.serializer | ||||||
|  | import kotlinx.serialization.descriptors.* | ||||||
|  | import kotlinx.serialization.encoding.* | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Realization of this interface will contains at least one not null - [optionalT1] or [optionalT2] | ||||||
|  |  * | ||||||
|  |  * @see EitherFirst | ||||||
|  |  * @see EitherSecond | ||||||
|  |  * @see Either.Companion.first | ||||||
|  |  * @see Either.Companion.second | ||||||
|  |  * @see Either.onFirst | ||||||
|  |  * @see Either.onSecond | ||||||
|  |  * @see Either.mapOnFirst | ||||||
|  |  * @see Either.mapOnSecond | ||||||
|  |  */ | ||||||
|  | @Serializable(EitherSerializer::class) | ||||||
|  | sealed interface Either<T1, T2> { | ||||||
|  |     val optionalT1: Optional<T1> | ||||||
|  |     val optionalT2: Optional<T2> | ||||||
|  |  | ||||||
|  |     val t1OrNull: T1? | ||||||
|  |         get() = optionalT1.dataOrNull() | ||||||
|  |     val t2OrNull: T2? | ||||||
|  |         get() = optionalT2.dataOrNull() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class EitherSerializer<T1, T2>( | ||||||
|  |     t1Serializer: KSerializer<T1>, | ||||||
|  |     t2Serializer: KSerializer<T2>, | ||||||
|  | ) : KSerializer<Either<T1, T2>> { | ||||||
|  |     @OptIn(InternalSerializationApi::class, ExperimentalSerializationApi::class) | ||||||
|  |     override val descriptor: SerialDescriptor = buildSerialDescriptor( | ||||||
|  |         "TypedSerializer", | ||||||
|  |         SerialKind.CONTEXTUAL | ||||||
|  |     ) { | ||||||
|  |         element("type", String.serializer().descriptor) | ||||||
|  |         element("value", ContextualSerializer(Either::class).descriptor) | ||||||
|  |     } | ||||||
|  |     private val t1EitherSerializer = EitherFirst.serializer(t1Serializer, t2Serializer) | ||||||
|  |     private val t2EitherSerializer = EitherSecond.serializer(t1Serializer, t2Serializer) | ||||||
|  |  | ||||||
|  |     override fun deserialize(decoder: Decoder): Either<T1, T2> { | ||||||
|  |         return decoder.decodeStructure(descriptor) { | ||||||
|  |             var type: String? = null | ||||||
|  |             lateinit var result: Either<T1, T2> | ||||||
|  |             while (true) { | ||||||
|  |                 when (val index = decodeElementIndex(descriptor)) { | ||||||
|  |                     0 -> type = decodeStringElement(descriptor, 0) | ||||||
|  |                     1 -> { | ||||||
|  |                         result = when (type) { | ||||||
|  |                             "t1" -> decodeSerializableElement( | ||||||
|  |                                 descriptor, | ||||||
|  |                                 1, | ||||||
|  |                                 t1EitherSerializer | ||||||
|  |                             ) | ||||||
|  |                             "t2" -> decodeSerializableElement( | ||||||
|  |                                 descriptor, | ||||||
|  |                                 1, | ||||||
|  |                                 t2EitherSerializer | ||||||
|  |                             ) | ||||||
|  |                             else -> error("Unknown type of either: $type") | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                     CompositeDecoder.DECODE_DONE -> break | ||||||
|  |                     else -> error("Unexpected index: $index") | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             result | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     override fun serialize(encoder: Encoder, value: Either<T1, T2>) { | ||||||
|  |         encoder.encodeStructure(descriptor) { | ||||||
|  |             when (value) { | ||||||
|  |                 is EitherFirst -> { | ||||||
|  |                     encodeStringElement(descriptor, 0, "t1") | ||||||
|  |                     encodeSerializableElement(descriptor, 1, t1EitherSerializer, value) | ||||||
|  |                 } | ||||||
|  |                 is EitherSecond -> { | ||||||
|  |                     encodeStringElement(descriptor, 0, "t2") | ||||||
|  |                     encodeSerializableElement(descriptor, 1, t2EitherSerializer, value) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * This type [Either] will always have not nullable [optionalT1] | ||||||
|  |  */ | ||||||
|  | @Serializable | ||||||
|  | data class EitherFirst<T1, T2>( | ||||||
|  |     val t1: T1 | ||||||
|  | ) : Either<T1, T2> { | ||||||
|  |     override val optionalT1: Optional<T1> = t1.optional | ||||||
|  |     override val optionalT2: Optional<T2> = Optional.absent() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * This type [Either] will always have not nullable [optionalT2] | ||||||
|  |  */ | ||||||
|  | @Serializable | ||||||
|  | data class EitherSecond<T1, T2>( | ||||||
|  |     val t2: T2 | ||||||
|  | ) : Either<T1, T2> { | ||||||
|  |     override val optionalT1: Optional<T1> = Optional.absent() | ||||||
|  |     override val optionalT2: Optional<T2> = t2.optional | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @return New instance of [EitherFirst] | ||||||
|  |  */ | ||||||
|  | inline fun <T1, T2> Either.Companion.first(t1: T1): Either<T1, T2> = EitherFirst(t1) | ||||||
|  | /** | ||||||
|  |  * @return New instance of [EitherSecond] | ||||||
|  |  */ | ||||||
|  | inline fun <T1, T2> Either.Companion.second(t2: T2): Either<T1, T2> = EitherSecond(t2) | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Will call [block] in case when [this] is [EitherFirst] | ||||||
|  |  */ | ||||||
|  | inline fun <T1, T2, E : Either<T1, T2>> E.onFirst(block: (T1) -> Unit): E { | ||||||
|  |     optionalT1.onPresented(block) | ||||||
|  |     return this | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Will call [block] in case when [this] is [EitherSecond] | ||||||
|  |  */ | ||||||
|  | inline fun <T1, T2, E : Either<T1, T2>> E.onSecond(block: (T2) -> Unit): E { | ||||||
|  |     optionalT2.onPresented(block) | ||||||
|  |     return this | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @return Result of [block] if [this] is [EitherFirst] | ||||||
|  |  */ | ||||||
|  | inline fun <T1, R> Either<T1, *>.mapOnFirst(block: (T1) -> R): R? { | ||||||
|  |     return optionalT1.mapOnPresented(block) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @return Result of [block] if [this] is [EitherSecond] | ||||||
|  |  */ | ||||||
|  | inline fun <T2, R> Either<*, T2>.mapOnSecond(block: (T2) -> R): R? { | ||||||
|  |     return optionalT2.mapOnPresented(block) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | inline fun <reified T1, reified T2> Any.either() = when (this) { | ||||||
|  |     is T1 -> Either.first<T1, T2>(this) | ||||||
|  |     is T2 -> Either.second<T1, T2>(this) | ||||||
|  |     else -> error("Incorrect type of either argument $this") | ||||||
|  | } | ||||||
| @@ -0,0 +1,43 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | inline fun <T> Boolean.letIfTrue(block: () -> T): T? { | ||||||
|  |     return if (this) { | ||||||
|  |         block() | ||||||
|  |     } else { | ||||||
|  |         null | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | inline fun <T> Boolean.letIfFalse(block: () -> T): T? { | ||||||
|  |     return if (this) { | ||||||
|  |         null | ||||||
|  |     } else { | ||||||
|  |         block() | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | inline fun Boolean.alsoIfTrue(block: () -> Unit): Boolean { | ||||||
|  |     letIfTrue(block) | ||||||
|  |     return this | ||||||
|  | } | ||||||
|  |  | ||||||
|  | inline fun Boolean.alsoIfFalse(block: () -> Unit): Boolean { | ||||||
|  |     letIfFalse(block) | ||||||
|  |     return this | ||||||
|  | } | ||||||
|  |  | ||||||
|  | inline fun <T> Boolean.ifTrue(block: () -> T): T? { | ||||||
|  |     return if (this) { | ||||||
|  |         block() | ||||||
|  |     } else { | ||||||
|  |         null | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | inline fun <T> Boolean.ifFalse(block: () -> T): T? { | ||||||
|  |     return if (this) { | ||||||
|  |         null | ||||||
|  |     } else { | ||||||
|  |         block() | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,30 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | import kotlinx.serialization.KSerializer | ||||||
|  | import kotlinx.serialization.Serializer | ||||||
|  | import kotlinx.serialization.builtins.PairSerializer | ||||||
|  | import kotlinx.serialization.builtins.serializer | ||||||
|  | import kotlinx.serialization.descriptors.SerialDescriptor | ||||||
|  | import kotlinx.serialization.encoding.Decoder | ||||||
|  | import kotlinx.serialization.encoding.Encoder | ||||||
|  |  | ||||||
|  | class IndexedValueSerializer<T>(private val subSerializer: KSerializer<T>) : KSerializer<IndexedValue<T>> { | ||||||
|  |     private val originalSerializer = PairSerializer(Int.serializer(), subSerializer) | ||||||
|  |     override val descriptor: SerialDescriptor | ||||||
|  |         get() = originalSerializer.descriptor | ||||||
|  |  | ||||||
|  |     override fun deserialize(decoder: Decoder): IndexedValue<T> { | ||||||
|  |         val pair = originalSerializer.deserialize(decoder) | ||||||
|  |         return IndexedValue( | ||||||
|  |             pair.first, | ||||||
|  |             pair.second | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun serialize(encoder: Encoder, value: IndexedValue<T>) { | ||||||
|  |         originalSerializer.serialize( | ||||||
|  |             encoder, | ||||||
|  |             Pair(value.index, value.value) | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -1,10 +1,10 @@ | |||||||
| package dev.inmo.micro_utils.common | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
| inline fun <I, R> Iterable<I>.joinTo( | inline fun <I, R> Iterable<I>.joinTo( | ||||||
|     crossinline separatorFun: (I) -> R?, |     separatorFun: (I) -> R?, | ||||||
|     prefix: R? = null, |     prefix: R? = null, | ||||||
|     postfix: R? = null, |     postfix: R? = null, | ||||||
|     crossinline transform: (I) -> R? |     transform: (I) -> R? | ||||||
| ): List<R> { | ): List<R> { | ||||||
|     val result = mutableListOf<R>() |     val result = mutableListOf<R>() | ||||||
|     val iterator = iterator() |     val iterator = iterator() | ||||||
| @@ -29,11 +29,11 @@ inline fun <I, R> Iterable<I>.joinTo( | |||||||
|     separator: R? = null, |     separator: R? = null, | ||||||
|     prefix: R? = null, |     prefix: R? = null, | ||||||
|     postfix: R? = null, |     postfix: R? = null, | ||||||
|     crossinline transform: (I) -> R? |     transform: (I) -> R? | ||||||
| ): List<R> = joinTo({ separator }, prefix, postfix, transform) | ): List<R> = joinTo({ separator }, prefix, postfix, transform) | ||||||
|  |  | ||||||
| inline fun <I> Iterable<I>.joinTo( | inline fun <I> Iterable<I>.joinTo( | ||||||
|     crossinline separatorFun: (I) -> I?, |     separatorFun: (I) -> I?, | ||||||
|     prefix: I? = null, |     prefix: I? = null, | ||||||
|     postfix: I? = null |     postfix: I? = null | ||||||
| ): List<I> = joinTo<I, I>(separatorFun, prefix, postfix) { it } | ): List<I> = joinTo<I, I>(separatorFun, prefix, postfix) { it } | ||||||
| @@ -45,15 +45,15 @@ inline fun <I> Iterable<I>.joinTo( | |||||||
| ): List<I> = joinTo<I>({ separator }, prefix, postfix) | ): List<I> = joinTo<I>({ separator }, prefix, postfix) | ||||||
|  |  | ||||||
| inline fun <I, reified R> Array<I>.joinTo( | inline fun <I, reified R> Array<I>.joinTo( | ||||||
|     crossinline separatorFun: (I) -> R?, |     separatorFun: (I) -> R?, | ||||||
|     prefix: R? = null, |     prefix: R? = null, | ||||||
|     postfix: R? = null, |     postfix: R? = null, | ||||||
|     crossinline transform: (I) -> R? |     transform: (I) -> R? | ||||||
| ): Array<R> = asIterable().joinTo(separatorFun, prefix, postfix, transform).toTypedArray() | ): Array<R> = asIterable().joinTo(separatorFun, prefix, postfix, transform).toTypedArray() | ||||||
|  |  | ||||||
| inline fun <I, reified R> Array<I>.joinTo( | inline fun <I, reified R> Array<I>.joinTo( | ||||||
|     separator: R? = null, |     separator: R? = null, | ||||||
|     prefix: R? = null, |     prefix: R? = null, | ||||||
|     postfix: R? = null, |     postfix: R? = null, | ||||||
|     crossinline transform: (I) -> R? |     transform: (I) -> R? | ||||||
| ): Array<R> = asIterable().joinTo(separator, prefix, postfix, transform).toTypedArray() | ): Array<R> = asIterable().joinTo(separator, prefix, postfix, transform).toTypedArray() | ||||||
|   | |||||||
| @@ -23,11 +23,12 @@ value class FileName(val string: String) { | |||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
| @PreviewFeature |  | ||||||
| expect class MPPFile | expect class MPPFile | ||||||
|  |  | ||||||
| expect val MPPFile.filename: FileName | expect val MPPFile.filename: FileName | ||||||
| expect val MPPFile.filesize: Long | expect val MPPFile.filesize: Long | ||||||
|  | expect val MPPFile.bytesAllocatorSync: ByteArrayAllocator | ||||||
| expect val MPPFile.bytesAllocator: SuspendByteArrayAllocator | expect val MPPFile.bytesAllocator: SuspendByteArrayAllocator | ||||||
|  | fun MPPFile.bytesSync() = bytesAllocatorSync() | ||||||
| suspend fun MPPFile.bytes() = bytesAllocator() | suspend fun MPPFile.bytes() = bytesAllocator() | ||||||
|  |  | ||||||
|   | |||||||
| @@ -0,0 +1,135 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Contains diff based on the comparison of objects with the same [K]. | ||||||
|  |  * | ||||||
|  |  * @param removed Contains map with keys removed from parent map | ||||||
|  |  * @param changed Contains map with keys values changed new map in comparison with old one | ||||||
|  |  * @param added Contains map with new keys and values | ||||||
|  |  */ | ||||||
|  | data class MapDiff<K, V> @Warning(warning) constructor( | ||||||
|  |     val removed: Map<K, V>, | ||||||
|  |     val changed: Map<K, Pair<V, V>>, | ||||||
|  |     val added: Map<K, V> | ||||||
|  | ) { | ||||||
|  |     fun isEmpty() = removed.isEmpty() && changed.isEmpty() && added.isEmpty() | ||||||
|  |     inline fun isNotEmpty() = !isEmpty() | ||||||
|  |  | ||||||
|  |     companion object { | ||||||
|  |         private const val warning = "This feature can be changed without any warranties. Use with caution and only in case you know what you are doing" | ||||||
|  |         fun <K, V> empty() = MapDiff<K, V>(emptyMap(), emptyMap(), emptyMap()) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | private inline fun <K, V> createCompareFun( | ||||||
|  |     strictComparison: Boolean | ||||||
|  | ): (K, V, V) -> Boolean = if (strictComparison) { | ||||||
|  |     { _, first, second -> first === second } | ||||||
|  | } else { | ||||||
|  |     { _, first, second -> first == second } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Compare [this] [Map] with the [other] one in principle when [other] is newer than [this] | ||||||
|  |  * | ||||||
|  |  * @param compareFun Will be used to determine changed values | ||||||
|  |  */ | ||||||
|  | fun <K, V> Map<K, V>.diff( | ||||||
|  |     other: Map<K, V>, | ||||||
|  |     compareFun: (K, V, V) -> Boolean | ||||||
|  | ): MapDiff<K, V> { | ||||||
|  |     val removed: Map<K, V> = (keys - other.keys).associateWith { | ||||||
|  |         getValue(it) | ||||||
|  |     } | ||||||
|  |     val added: Map<K, V> = (other.keys - keys).associateWith { | ||||||
|  |         other.getValue(it) | ||||||
|  |     } | ||||||
|  |     val changed = keys.intersect(other.keys).mapNotNull { | ||||||
|  |         val old = getValue(it) | ||||||
|  |         val new = other.getValue(it) | ||||||
|  |         if (compareFun(it, old, new)) { | ||||||
|  |             return@mapNotNull null | ||||||
|  |         } else { | ||||||
|  |             it to (old to new) | ||||||
|  |         } | ||||||
|  |     }.toMap() | ||||||
|  |  | ||||||
|  |     return MapDiff( | ||||||
|  |         removed, | ||||||
|  |         changed, | ||||||
|  |         added | ||||||
|  |     ) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Compare [this] [Map] with the [other] one in principle when [other] is newer than [this] | ||||||
|  |  * | ||||||
|  |  * @param strictComparison If true, will use strict (===) comparison for the values' comparison. Otherwise, standard | ||||||
|  |  * `equals` will be used | ||||||
|  |  */ | ||||||
|  | fun <K, V> Map<K, V>.diff( | ||||||
|  |     other: Map<K, V>, | ||||||
|  |     strictComparison: Boolean = false | ||||||
|  | ): MapDiff<K, V> = diff( | ||||||
|  |     other, | ||||||
|  |     compareFun = createCompareFun(strictComparison) | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Will apply [mapDiff] to [this] [MutableMap] | ||||||
|  |  */ | ||||||
|  | fun <K, V> MutableMap<K, V>.applyDiff( | ||||||
|  |     mapDiff: MapDiff<K, V> | ||||||
|  | ) { | ||||||
|  |     mapDiff.apply { | ||||||
|  |         removed.keys.forEach { remove(it) } | ||||||
|  |         changed.forEach { (k, oldNew) -> | ||||||
|  |             put(k, oldNew.second) | ||||||
|  |         } | ||||||
|  |         added.forEach { (k, new) -> | ||||||
|  |             put(k, new) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Will apply changes with [from] map into [this] one | ||||||
|  |  * | ||||||
|  |  * @param compareFun Will be used to determine changed values | ||||||
|  |  * | ||||||
|  |  * @return [MapDiff] applied to [this] [MutableMap] | ||||||
|  |  */ | ||||||
|  | fun <K, V> MutableMap<K, V>.applyDiff( | ||||||
|  |     from: Map<K, V>, | ||||||
|  |     compareFun: (K, V, V) -> Boolean | ||||||
|  | ): MapDiff<K, V> { | ||||||
|  |     return diff(from, compareFun).also { | ||||||
|  |         applyDiff(it) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Will apply changes with [from] map into [this] one | ||||||
|  |  * | ||||||
|  |  * @param strictComparison If true, will use strict (===) comparison for the values' comparison. Otherwise, standard | ||||||
|  |  * `equals` will be used | ||||||
|  |  * | ||||||
|  |  * @return [MapDiff] applied to [this] [MutableMap] | ||||||
|  |  */ | ||||||
|  | fun <K, V> MutableMap<K, V>.applyDiff( | ||||||
|  |     from: Map<K, V>, | ||||||
|  |     strictComparison: Boolean = false | ||||||
|  | ): MapDiff<K, V> = applyDiff( | ||||||
|  |     from, | ||||||
|  |     compareFun = createCompareFun(strictComparison) | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Reverse [this] [MapDiff]. Result will contain [MapDiff.added] on [MapDiff.removed] (and vice-verse), all the | ||||||
|  |  * [MapDiff.changed] values will be reversed too | ||||||
|  |  */ | ||||||
|  | fun <K, V> MapDiff<K, V>.reversed(): MapDiff<K, V> = MapDiff( | ||||||
|  |     removed = added, | ||||||
|  |     changed = changed.mapValues { (_, oldNew) -> oldNew.second to oldNew.first }, | ||||||
|  |     added = removed | ||||||
|  | ) | ||||||
| @@ -0,0 +1,53 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | import kotlin.jvm.JvmName | ||||||
|  |  | ||||||
|  | interface SimpleMapper<T1, T2> { | ||||||
|  |     fun convertToT1(from: T2): T1 | ||||||
|  |     fun convertToT2(from: T1): T2 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @JvmName("convertFromT2") | ||||||
|  | fun <T1, T2> SimpleMapper<T1, T2>.convert(from: T2) = convertToT1(from) | ||||||
|  | @JvmName("convertFromT1") | ||||||
|  | fun <T1, T2> SimpleMapper<T1, T2>.convert(from: T1) = convertToT2(from) | ||||||
|  |  | ||||||
|  | class SimpleMapperImpl<T1, T2>( | ||||||
|  |     private val t1: (T2) -> T1, | ||||||
|  |     private val t2: (T1) -> T2, | ||||||
|  | ) : SimpleMapper<T1, T2> { | ||||||
|  |     override fun convertToT1(from: T2): T1 = t1.invoke(from) | ||||||
|  |  | ||||||
|  |     override fun convertToT2(from: T1): T2 = t2.invoke(from) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @Suppress("NOTHING_TO_INLINE") | ||||||
|  | inline fun <T1, T2> simpleMapper( | ||||||
|  |     noinline t1: (T2) -> T1, | ||||||
|  |     noinline t2: (T1) -> T2, | ||||||
|  | ) = SimpleMapperImpl(t1, t2) | ||||||
|  |  | ||||||
|  | interface SimpleSuspendableMapper<T1, T2> { | ||||||
|  |     suspend fun convertToT1(from: T2): T1 | ||||||
|  |     suspend fun convertToT2(from: T1): T2 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @JvmName("convertFromT2") | ||||||
|  | suspend fun <T1, T2> SimpleSuspendableMapper<T1, T2>.convert(from: T2) = convertToT1(from) | ||||||
|  | @JvmName("convertFromT1") | ||||||
|  | suspend fun <T1, T2> SimpleSuspendableMapper<T1, T2>.convert(from: T1) = convertToT2(from) | ||||||
|  |  | ||||||
|  | class SimpleSuspendableMapperImpl<T1, T2>( | ||||||
|  |     private val t1: suspend (T2) -> T1, | ||||||
|  |     private val t2: suspend (T1) -> T2, | ||||||
|  | ) : SimpleSuspendableMapper<T1, T2> { | ||||||
|  |     override suspend fun convertToT1(from: T2): T1 = t1.invoke(from) | ||||||
|  |  | ||||||
|  |     override suspend fun convertToT2(from: T1): T2 = t2.invoke(from) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @Suppress("NOTHING_TO_INLINE") | ||||||
|  | inline fun <T1, T2> simpleSuspendableMapper( | ||||||
|  |     noinline t1: suspend (T2) -> T1, | ||||||
|  |     noinline t2: suspend (T1) -> T2, | ||||||
|  | ) = SimpleSuspendableMapperImpl(t1, t2) | ||||||
| @@ -0,0 +1,97 @@ | |||||||
|  | @file:Suppress("unused") | ||||||
|  |  | ||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | import kotlinx.serialization.Serializable | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * This type represents [T] as not only potentially nullable data, but also as a data which can not be presented. This | ||||||
|  |  * type will be useful in cases when [T] is nullable and null as valuable data too in time of data absence should be | ||||||
|  |  * presented by some third type. | ||||||
|  |  * | ||||||
|  |  * Let's imagine, you have nullable name in some database. In case when name is not nullable everything is clear - null | ||||||
|  |  * will represent absence of row in the database. In case when name is nullable null will be a little bit dual-meaning, | ||||||
|  |  * cause this null will say nothing about availability of the row (of course, it is exaggerated example) | ||||||
|  |  * | ||||||
|  |  * @see Optional.presented | ||||||
|  |  * @see Optional.absent | ||||||
|  |  * @see Optional.optional | ||||||
|  |  * @see Optional.onPresented | ||||||
|  |  * @see Optional.onAbsent | ||||||
|  |  */ | ||||||
|  | @Serializable | ||||||
|  | data class Optional<T> internal constructor( | ||||||
|  |     @Warning("It is unsafe to use this data directly") | ||||||
|  |     val data: T?, | ||||||
|  |     @Warning("It is unsafe to use this data directly") | ||||||
|  |     val dataPresented: Boolean | ||||||
|  | ) { | ||||||
|  |     companion object { | ||||||
|  |         /** | ||||||
|  |          * Will create [Optional] with presented data | ||||||
|  |          */ | ||||||
|  |         fun <T> presented(data: T) = Optional(data, true) | ||||||
|  |         /** | ||||||
|  |          * Will create [Optional] without data | ||||||
|  |          */ | ||||||
|  |         fun <T> absent() = Optional<T>(null, false) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | inline val <T> T.optional | ||||||
|  |     get() = Optional.presented(this) | ||||||
|  |  | ||||||
|  | inline val <T : Any> T?.optionalOrAbsentIfNull | ||||||
|  |     get() = if (this == null) { | ||||||
|  |         Optional.absent<T>() | ||||||
|  |     } else { | ||||||
|  |         Optional.presented(this) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Will call [block] when data presented ([Optional.dataPresented] == true) | ||||||
|  |  */ | ||||||
|  | inline fun <T> Optional<T>.onPresented(block: (T) -> Unit): Optional<T> = apply { | ||||||
|  |     @OptIn(Warning::class) | ||||||
|  |     if (dataPresented) { @Suppress("UNCHECKED_CAST") block(data as T) } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Will call [block] when data presented ([Optional.dataPresented] == true) | ||||||
|  |  */ | ||||||
|  | inline fun <T, R> Optional<T>.mapOnPresented(block: (T) -> R): R? = run { | ||||||
|  |     @OptIn(Warning::class) | ||||||
|  |     if (dataPresented) { @Suppress("UNCHECKED_CAST") block(data as T) } else null | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Will call [block] when data absent ([Optional.dataPresented] == false) | ||||||
|  |  */ | ||||||
|  | inline fun <T> Optional<T>.onAbsent(block: () -> Unit): Optional<T> = apply { | ||||||
|  |     @OptIn(Warning::class) | ||||||
|  |     if (!dataPresented) { block() } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Will call [block] when data presented ([Optional.dataPresented] == true) | ||||||
|  |  */ | ||||||
|  | inline fun <T, R> Optional<T>.mapOnAbsent(block: () -> R): R? = run { | ||||||
|  |     @OptIn(Warning::class) | ||||||
|  |     if (!dataPresented) { block() } else null | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Returns [Optional.data] if [Optional.dataPresented] of [this] is true, or null otherwise | ||||||
|  |  */ | ||||||
|  | fun <T> Optional<T>.dataOrNull() = @OptIn(Warning::class) if (dataPresented) @Suppress("UNCHECKED_CAST") (data as T) else null | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Returns [Optional.data] if [Optional.dataPresented] of [this] is true, or throw [throwable] otherwise | ||||||
|  |  */ | ||||||
|  | fun <T> Optional<T>.dataOrThrow(throwable: Throwable) = @OptIn(Warning::class) if (dataPresented) @Suppress("UNCHECKED_CAST") (data as T) else throw throwable | ||||||
|  |  | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Returns [Optional.data] if [Optional.dataPresented] of [this] is true, or call [block] and returns the result of it | ||||||
|  |  */ | ||||||
|  | inline fun <T> Optional<T>.dataOrElse(block: () -> T) = @OptIn(Warning::class) if (dataPresented) @Suppress("UNCHECKED_CAST") (data as T) else block() | ||||||
| @@ -0,0 +1,28 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Convert [this] [Long] to [Int] with bounds of [Int.MIN_VALUE] and [Int.MAX_VALUE] | ||||||
|  |  */ | ||||||
|  | fun Long.toCoercedInt(): Int = coerceIn(Int.MIN_VALUE.toLong(), Int.MAX_VALUE.toLong()).toInt() | ||||||
|  | /** | ||||||
|  |  * Convert [this] [Long] to [Short] with bounds of [Short.MIN_VALUE] and [Short.MAX_VALUE] | ||||||
|  |  */ | ||||||
|  | fun Long.toCoercedShort(): Short = coerceIn(Short.MIN_VALUE.toLong(), Short.MAX_VALUE.toLong()).toShort() | ||||||
|  | /** | ||||||
|  |  * Convert [this] [Long] to [Byte] with bounds of [Byte.MIN_VALUE] and [Byte.MAX_VALUE] | ||||||
|  |  */ | ||||||
|  | fun Long.toCoercedByte(): Byte = coerceIn(Byte.MIN_VALUE.toLong(), Byte.MAX_VALUE.toLong()).toByte() | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Convert [this] [Int] to [Short] with bounds of [Short.MIN_VALUE] and [Short.MAX_VALUE] | ||||||
|  |  */ | ||||||
|  | fun Int.toCoercedShort(): Short = coerceIn(Short.MIN_VALUE.toInt(), Short.MAX_VALUE.toInt()).toShort() | ||||||
|  | /** | ||||||
|  |  * Convert [this] [Int] to [Byte] with bounds of [Byte.MIN_VALUE] and [Byte.MAX_VALUE] | ||||||
|  |  */ | ||||||
|  | fun Int.toCoercedByte(): Byte = coerceIn(Byte.MIN_VALUE.toInt(), Byte.MAX_VALUE.toInt()).toByte() | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Convert [this] [Short] to [Byte] with bounds of [Byte.MIN_VALUE] and [Byte.MAX_VALUE] | ||||||
|  |  */ | ||||||
|  | fun Short.toCoercedByte(): Byte = coerceIn(Byte.MIN_VALUE.toShort(), Byte.MAX_VALUE.toShort()).toByte() | ||||||
| @@ -0,0 +1,37 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | import kotlinx.serialization.Serializable | ||||||
|  | import kotlin.jvm.JvmInline | ||||||
|  |  | ||||||
|  | @Serializable | ||||||
|  | @JvmInline | ||||||
|  | value class Progress private constructor( | ||||||
|  |     val of1: Double | ||||||
|  | ) { | ||||||
|  |     val of1Float | ||||||
|  |         get() = of1.toFloat() | ||||||
|  |     val of100 | ||||||
|  |         get() = of1 * 100 | ||||||
|  |     val of100Float | ||||||
|  |         get() = of100.toFloat() | ||||||
|  |     val of100Int | ||||||
|  |         get() = of100.toInt() | ||||||
|  |  | ||||||
|  |     init { | ||||||
|  |         require(of1 in rangeOfValues) { | ||||||
|  |             "Progress main value should be in $rangeOfValues, but incoming value is $of1" | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     companion object { | ||||||
|  |         val rangeOfValues = 0.0 .. 1.0 | ||||||
|  |  | ||||||
|  |         val START = Progress(rangeOfValues.start) | ||||||
|  |         val COMPLETED = Progress(rangeOfValues.endInclusive) | ||||||
|  |  | ||||||
|  |         operator fun invoke(of1: Double) = Progress(of1.coerceIn(rangeOfValues)) | ||||||
|  |         operator fun invoke(part: Number, total: Number) = Progress( | ||||||
|  |             part.toDouble() / total.toDouble() | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,80 @@ | |||||||
|  | @file:Suppress( | ||||||
|  |   "RemoveRedundantCallsOfConversionMethods", | ||||||
|  |   "RedundantVisibilityModifier", | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | import kotlin.Byte | ||||||
|  | import kotlin.Double | ||||||
|  | import kotlin.Float | ||||||
|  | import kotlin.Int | ||||||
|  | import kotlin.Long | ||||||
|  | import kotlin.Short | ||||||
|  | import kotlin.Suppress | ||||||
|  |  | ||||||
|  | public operator fun Progress.plus(other: Progress): Progress = Progress(of1 + other.of1) | ||||||
|  |  | ||||||
|  | public operator fun Progress.minus(other: Progress): Progress = Progress(of1 - other.of1) | ||||||
|  |  | ||||||
|  | public operator fun Progress.plus(i: Byte): Progress = Progress((of1 + i).toDouble()) | ||||||
|  |  | ||||||
|  | public operator fun Progress.minus(i: Byte): Progress = Progress((of1 - i).toDouble()) | ||||||
|  |  | ||||||
|  | public operator fun Progress.times(i: Byte): Progress = Progress((of1 * i).toDouble()) | ||||||
|  |  | ||||||
|  | public operator fun Progress.div(i: Byte): Progress = Progress((of1 / i).toDouble()) | ||||||
|  |  | ||||||
|  | public operator fun Progress.rem(i: Byte): Progress = Progress((of1 % i).toDouble()) | ||||||
|  |  | ||||||
|  | public operator fun Progress.plus(i: Short): Progress = Progress((of1 + i).toDouble()) | ||||||
|  |  | ||||||
|  | public operator fun Progress.minus(i: Short): Progress = Progress((of1 - i).toDouble()) | ||||||
|  |  | ||||||
|  | public operator fun Progress.times(i: Short): Progress = Progress((of1 * i).toDouble()) | ||||||
|  |  | ||||||
|  | public operator fun Progress.div(i: Short): Progress = Progress((of1 / i).toDouble()) | ||||||
|  |  | ||||||
|  | public operator fun Progress.rem(i: Short): Progress = Progress((of1 % i).toDouble()) | ||||||
|  |  | ||||||
|  | public operator fun Progress.plus(i: Int): Progress = Progress((of1 + i).toDouble()) | ||||||
|  |  | ||||||
|  | public operator fun Progress.minus(i: Int): Progress = Progress((of1 - i).toDouble()) | ||||||
|  |  | ||||||
|  | public operator fun Progress.times(i: Int): Progress = Progress((of1 * i).toDouble()) | ||||||
|  |  | ||||||
|  | public operator fun Progress.div(i: Int): Progress = Progress((of1 / i).toDouble()) | ||||||
|  |  | ||||||
|  | public operator fun Progress.rem(i: Int): Progress = Progress((of1 % i).toDouble()) | ||||||
|  |  | ||||||
|  | public operator fun Progress.plus(i: Long): Progress = Progress((of1 + i).toDouble()) | ||||||
|  |  | ||||||
|  | public operator fun Progress.minus(i: Long): Progress = Progress((of1 - i).toDouble()) | ||||||
|  |  | ||||||
|  | public operator fun Progress.times(i: Long): Progress = Progress((of1 * i).toDouble()) | ||||||
|  |  | ||||||
|  | public operator fun Progress.div(i: Long): Progress = Progress((of1 / i).toDouble()) | ||||||
|  |  | ||||||
|  | public operator fun Progress.rem(i: Long): Progress = Progress((of1 % i).toDouble()) | ||||||
|  |  | ||||||
|  | public operator fun Progress.plus(i: Float): Progress = Progress((of1 + i).toDouble()) | ||||||
|  |  | ||||||
|  | public operator fun Progress.minus(i: Float): Progress = Progress((of1 - i).toDouble()) | ||||||
|  |  | ||||||
|  | public operator fun Progress.times(i: Float): Progress = Progress((of1 * i).toDouble()) | ||||||
|  |  | ||||||
|  | public operator fun Progress.div(i: Float): Progress = Progress((of1 / i).toDouble()) | ||||||
|  |  | ||||||
|  | public operator fun Progress.rem(i: Float): Progress = Progress((of1 % i).toDouble()) | ||||||
|  |  | ||||||
|  | public operator fun Progress.plus(i: Double): Progress = Progress((of1 + i).toDouble()) | ||||||
|  |  | ||||||
|  | public operator fun Progress.minus(i: Double): Progress = Progress((of1 - i).toDouble()) | ||||||
|  |  | ||||||
|  | public operator fun Progress.times(i: Double): Progress = Progress((of1 * i).toDouble()) | ||||||
|  |  | ||||||
|  | public operator fun Progress.div(i: Double): Progress = Progress((of1 / i).toDouble()) | ||||||
|  |  | ||||||
|  | public operator fun Progress.rem(i: Double): Progress = Progress((of1 % i).toDouble()) | ||||||
|  |  | ||||||
|  | public operator fun Progress.compareTo(other: Progress): Int = (of1 - other.of1).toInt() | ||||||
| @@ -0,0 +1,19 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | fun <T : Comparable<T>> ClosedRange<T>.intersect(other: ClosedRange<T>): Pair<T, T>? = when { | ||||||
|  |     start == other.start && endInclusive == other.endInclusive -> start to endInclusive | ||||||
|  |     start > other.endInclusive || other.start > endInclusive -> null | ||||||
|  |     else -> maxOf(start, other.start) to minOf(endInclusive, other.endInclusive) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fun IntRange.intersect( | ||||||
|  |     other: IntRange | ||||||
|  | ): IntRange? = (this as ClosedRange<Int>).intersect(other as ClosedRange<Int>) ?.let { | ||||||
|  |     it.first .. it.second | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fun LongRange.intersect( | ||||||
|  |     other: LongRange | ||||||
|  | ): LongRange? = (this as ClosedRange<Long>).intersect(other as ClosedRange<Long>) ?.let { | ||||||
|  |     it.first .. it.second | ||||||
|  | } | ||||||
| @@ -0,0 +1,54 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Executes the given [action] until getting of successful result specified number of [times]. | ||||||
|  |  * | ||||||
|  |  * A zero-based index of current iteration is passed as a parameter to [action]. | ||||||
|  |  */ | ||||||
|  | inline fun <R> repeatOnFailure( | ||||||
|  |     onFailure: (Throwable) -> Boolean, | ||||||
|  |     action: () -> R | ||||||
|  | ): Result<R> { | ||||||
|  |     do { | ||||||
|  |         runCatching { | ||||||
|  |             action() | ||||||
|  |         }.onFailure { | ||||||
|  |             if (!onFailure(it)) { | ||||||
|  |                 return Result.failure(it) | ||||||
|  |             } | ||||||
|  |         }.onSuccess { | ||||||
|  |             return Result.success(it) | ||||||
|  |         } | ||||||
|  |     } while (true) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Executes the given [action] until getting of successful result specified number of [times]. | ||||||
|  |  * | ||||||
|  |  * A zero-based index of current iteration is passed as a parameter to [action]. | ||||||
|  |  */ | ||||||
|  | inline fun <R> repeatOnFailure( | ||||||
|  |     times: Int, | ||||||
|  |     onEachFailure: (Throwable) -> Unit = {}, | ||||||
|  |     action: (Int) -> R | ||||||
|  | ): Optional<R> { | ||||||
|  |     var i = 0 | ||||||
|  |     val result = repeatOnFailure( | ||||||
|  |         { | ||||||
|  |             onEachFailure(it) | ||||||
|  |             if (i < times) { | ||||||
|  |                 i++ | ||||||
|  |                 true | ||||||
|  |             } else { | ||||||
|  |                 false | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     ) { | ||||||
|  |         action(i) | ||||||
|  |     } | ||||||
|  |     return if (result.isSuccess) { | ||||||
|  |         Optional.presented(result.getOrThrow()) | ||||||
|  |     } else { | ||||||
|  |         Optional.absent() | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,6 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | val FixedSignsRange = 0 .. 100 | ||||||
|  |  | ||||||
|  | expect fun Float.fixed(signs: Int): Float | ||||||
|  | expect fun Double.fixed(signs: Int): Double | ||||||
| @@ -32,7 +32,7 @@ class DiffUtilsTests { | |||||||
|         val withIndex = oldList.withIndex() |         val withIndex = oldList.withIndex() | ||||||
|  |  | ||||||
|         for (count in 1 .. (floor(oldList.size.toFloat() / 2).toInt())) { |         for (count in 1 .. (floor(oldList.size.toFloat() / 2).toInt())) { | ||||||
|             for ((i, v) in withIndex) { |             for ((i, _) in withIndex) { | ||||||
|                 if (i + count > oldList.lastIndex) { |                 if (i + count > oldList.lastIndex) { | ||||||
|                     continue |                     continue | ||||||
|                 } |                 } | ||||||
| @@ -54,8 +54,8 @@ class DiffUtilsTests { | |||||||
|         val oldList = (0 until 10).map { it.toString() } |         val oldList = (0 until 10).map { it.toString() } | ||||||
|         val withIndex = oldList.withIndex() |         val withIndex = oldList.withIndex() | ||||||
|  |  | ||||||
|         for (step in 0 until oldList.size) { |         for (step in oldList.indices) { | ||||||
|             for ((i, v) in withIndex) { |             for ((i, _) in withIndex) { | ||||||
|                 val mutable = oldList.toMutableList() |                 val mutable = oldList.toMutableList() | ||||||
|                 val changes = ( |                 val changes = ( | ||||||
|                     if (step == 0) i until oldList.size else (i until oldList.size step step) |                     if (step == 0) i until oldList.size else (i until oldList.size step step) | ||||||
| @@ -73,4 +73,83 @@ class DiffUtilsTests { | |||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     @Test | ||||||
|  |     fun testThatSimpleRemoveApplyWorks() { | ||||||
|  |         val oldList = (0 until 10).toList() | ||||||
|  |         val withIndex = oldList.withIndex() | ||||||
|  |  | ||||||
|  |         for (count in 1 .. (floor(oldList.size.toFloat() / 2).toInt())) { | ||||||
|  |             for ((i, _) in withIndex) { | ||||||
|  |                 if (i + count > oldList.lastIndex) { | ||||||
|  |                     continue | ||||||
|  |                 } | ||||||
|  |                 val removedSublist = oldList.subList(i, i + count) | ||||||
|  |                 val mutableOldList = oldList.toMutableList() | ||||||
|  |                 val targetList = oldList - removedSublist | ||||||
|  |  | ||||||
|  |                 mutableOldList.applyDiff(targetList) | ||||||
|  |  | ||||||
|  |                 assertEquals( | ||||||
|  |                     targetList, | ||||||
|  |                     mutableOldList | ||||||
|  |                 ) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Test | ||||||
|  |     fun testThatSimpleAddApplyWorks() { | ||||||
|  |         val oldList = (0 until 10).map { it.toString() } | ||||||
|  |         val withIndex = oldList.withIndex() | ||||||
|  |  | ||||||
|  |         for (count in 1 .. (floor(oldList.size.toFloat() / 2).toInt())) { | ||||||
|  |             for ((i, _) in withIndex) { | ||||||
|  |                 if (i + count > oldList.lastIndex) { | ||||||
|  |                     continue | ||||||
|  |                 } | ||||||
|  |                 val addedSublist = oldList.subList(i, i + count).map { "added$it" } | ||||||
|  |                 val mutable = oldList.toMutableList() | ||||||
|  |                 mutable.addAll(i, addedSublist) | ||||||
|  |                 val mutableOldList = oldList.toMutableList() | ||||||
|  |  | ||||||
|  |                 mutableOldList.applyDiff(mutable) | ||||||
|  |  | ||||||
|  |                 assertEquals( | ||||||
|  |                     mutable, | ||||||
|  |                     mutableOldList | ||||||
|  |                 ) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Test | ||||||
|  |     fun testThatSimpleChangesApplyWorks() { | ||||||
|  |         val oldList = (0 until 10).map { it.toString() } | ||||||
|  |         val withIndex = oldList.withIndex() | ||||||
|  |  | ||||||
|  |         for (step in oldList.indices) { | ||||||
|  |             for ((i, _) in withIndex) { | ||||||
|  |                 val mutable = oldList.toMutableList() | ||||||
|  |  | ||||||
|  |                 val newList = if (step == 0) { | ||||||
|  |                     i until oldList.size | ||||||
|  |                 } else { | ||||||
|  |                     i until oldList.size step step | ||||||
|  |                 } | ||||||
|  |                 newList.forEach { index -> | ||||||
|  |                     IndexedValue(index, mutable[index]) to IndexedValue(index, "changed$index").also { | ||||||
|  |                         mutable[index] = it.value | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 val mutableOldList = oldList.toMutableList() | ||||||
|  |                 mutableOldList.applyDiff(mutable) | ||||||
|  |                 assertEquals( | ||||||
|  |                     mutable, | ||||||
|  |                     mutableOldList | ||||||
|  |                 ) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -0,0 +1,61 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | import kotlinx.browser.document | ||||||
|  | import org.w3c.dom.* | ||||||
|  |  | ||||||
|  | fun Node.onRemoved(block: () -> Unit): MutationObserver { | ||||||
|  |     lateinit var observer: MutationObserver | ||||||
|  |  | ||||||
|  |     observer = MutationObserver { _, _ -> | ||||||
|  |         fun checkIfRemoved(node: Node): Boolean { | ||||||
|  |             return node.parentNode != document && (node.parentNode ?.let { checkIfRemoved(it) } ?: true) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (checkIfRemoved(this)) { | ||||||
|  |             observer.disconnect() | ||||||
|  |             block() | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     observer.observe(document, MutationObserverInit(childList = true, subtree = true)) | ||||||
|  |     return observer | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fun Element.onVisibilityChanged(block: IntersectionObserverEntry.(Float, IntersectionObserver) -> Unit): IntersectionObserver { | ||||||
|  |     var previousIntersectionRatio = -1f | ||||||
|  |     val observer = IntersectionObserver { entries, observer -> | ||||||
|  |         entries.forEach { | ||||||
|  |             if (previousIntersectionRatio != it.intersectionRatio) { | ||||||
|  |                 previousIntersectionRatio = it.intersectionRatio.toFloat() | ||||||
|  |                 it.block(previousIntersectionRatio, observer) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     observer.observe(this) | ||||||
|  |     return observer | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fun Element.onVisible(block: Element.(IntersectionObserver) -> Unit) { | ||||||
|  |     var previous = -1f | ||||||
|  |     onVisibilityChanged { intersectionRatio, observer -> | ||||||
|  |         if (previous != intersectionRatio) { | ||||||
|  |             if (intersectionRatio > 0 && previous == 0f) { | ||||||
|  |                 block(observer) | ||||||
|  |             } | ||||||
|  |             previous = intersectionRatio | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fun Element.onInvisible(block: Element.(IntersectionObserver) -> Unit): IntersectionObserver { | ||||||
|  |     var previous = -1f | ||||||
|  |     return onVisibilityChanged { intersectionRatio, observer -> | ||||||
|  |         if (previous != intersectionRatio) { | ||||||
|  |             if (intersectionRatio == 0f && previous != 0f) { | ||||||
|  |                 block(observer) | ||||||
|  |             } | ||||||
|  |             previous = intersectionRatio | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,124 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | import org.w3c.dom.DOMRectReadOnly | ||||||
|  | import org.w3c.dom.Element | ||||||
|  |  | ||||||
|  | external interface IntersectionObserverOptions { | ||||||
|  |     /** | ||||||
|  |      * An Element or Document object which is an ancestor of the intended target, whose bounding rectangle will be | ||||||
|  |      * considered the viewport. Any part of the target not visible in the visible area of the root is not considered | ||||||
|  |      * visible. | ||||||
|  |      */ | ||||||
|  |     var root: Element? | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * A string which specifies a set of offsets to add to the root's bounding_box when calculating intersections, | ||||||
|  |      * effectively shrinking or growing the root for calculation purposes. The syntax is approximately the same as that | ||||||
|  |      * for the CSS margin property; see The root element and root margin in Intersection Observer API for more | ||||||
|  |      * information on how the margin works and the syntax. The default is "0px 0px 0px 0px". | ||||||
|  |      */ | ||||||
|  |     var rootMargin: String? | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Either a single number or an array of numbers between 0.0 and 1.0, specifying a ratio of intersection area to | ||||||
|  |      * total bounding box area for the observed target. A value of 0.0 means that even a single visible pixel counts as | ||||||
|  |      * the target being visible. 1.0 means that the entire target element is visible. See Thresholds in Intersection | ||||||
|  |      * Observer API for a more in-depth description of how thresholds are used. The default is a threshold of 0.0. | ||||||
|  |      */ | ||||||
|  |     var threshold: Array<Number>? | ||||||
|  | } | ||||||
|  | fun IntersectionObserverOptions( | ||||||
|  |     block: IntersectionObserverOptions.() -> Unit = {} | ||||||
|  | ): IntersectionObserverOptions = js("{}").unsafeCast<IntersectionObserverOptions>().apply(block) | ||||||
|  |  | ||||||
|  | external interface IntersectionObserverEntry { | ||||||
|  |     /** | ||||||
|  |      * Returns the bounds rectangle of the target element as a DOMRectReadOnly. The bounds are computed as described in | ||||||
|  |      * the documentation for Element.getBoundingClientRect(). | ||||||
|  |      */ | ||||||
|  |     val boundingClientRect: DOMRectReadOnly | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Returns the ratio of the intersectionRect to the boundingClientRect. | ||||||
|  |      */ | ||||||
|  |     val intersectionRatio: Number | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Returns a DOMRectReadOnly representing the target's visible area. | ||||||
|  |      */ | ||||||
|  |     val intersectionRect: DOMRectReadOnly | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * A Boolean value which is true if the target element intersects with the intersection observer's root. If this is | ||||||
|  |      * true, then, the IntersectionObserverEntry describes a transition into a state of intersection; if it's false, | ||||||
|  |      * then you know the transition is from intersecting to not-intersecting. | ||||||
|  |      */ | ||||||
|  |     val isIntersecting: Boolean | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Returns a DOMRectReadOnly for the intersection observer's root. | ||||||
|  |      */ | ||||||
|  |     val rootBounds: DOMRectReadOnly | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * The Element whose intersection with the root changed. | ||||||
|  |      */ | ||||||
|  |     val target: Element | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * A DOMHighResTimeStamp indicating the time at which the intersection was recorded, relative to the | ||||||
|  |      * IntersectionObserver's time origin. | ||||||
|  |      */ | ||||||
|  |     val time: Double | ||||||
|  | } | ||||||
|  |  | ||||||
|  | typealias IntersectionObserverCallback = (entries: Array<IntersectionObserverEntry>, observer: IntersectionObserver) -> Unit | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * This is just an implementation from [this commentary](https://youtrack.jetbrains.com/issue/KT-43157#focus=Comments-27-4498582.0-0) | ||||||
|  |  * of Kotlin JS issue related to the absence of [Intersection Observer API](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver) | ||||||
|  |  */ | ||||||
|  | external class IntersectionObserver(callback: IntersectionObserverCallback) { | ||||||
|  |     constructor(callback: IntersectionObserverCallback, options: IntersectionObserverOptions) | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * The Element or Document whose bounds are used as the bounding box when testing for intersection. If no root value | ||||||
|  |      * was passed to the constructor or its value is null, the top-level document's viewport is used. | ||||||
|  |      */ | ||||||
|  |     val root: Element | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * An offset rectangle applied to the root's bounding box when calculating intersections, effectively shrinking or | ||||||
|  |      * growing the root for calculation purposes. The value returned by this property may not be the same as the one | ||||||
|  |      * specified when calling the constructor as it may be changed to match internal requirements. Each offset can be | ||||||
|  |      * expressed in pixels (px) or as a percentage (%). The default is "0px 0px 0px 0px". | ||||||
|  |      */ | ||||||
|  |     val rootMargin: String | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * A list of thresholds, sorted in increasing numeric order, where each threshold is a ratio of intersection area to | ||||||
|  |      * bounding box area of an observed target. Notifications for a target are generated when any of the thresholds are | ||||||
|  |      * crossed for that target. If no value was passed to the constructor, 0 is used. | ||||||
|  |      */ | ||||||
|  |     val thresholds: Array<Number> | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Stops the IntersectionObserver object from observing any target. | ||||||
|  |      */ | ||||||
|  |     fun disconnect() | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Tells the IntersectionObserver a target element to observe. | ||||||
|  |      */ | ||||||
|  |     fun observe(targetElement: Element) | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Returns an array of IntersectionObserverEntry objects for all observed targets. | ||||||
|  |      */ | ||||||
|  |     fun takeRecords(): Array<IntersectionObserverEntry> | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Tells the IntersectionObserver to stop observing a particular target element. | ||||||
|  |      */ | ||||||
|  |     fun unobserve(targetElement: Element) | ||||||
|  | } | ||||||
| @@ -0,0 +1,12 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | import org.w3c.dom.Element | ||||||
|  |  | ||||||
|  | inline val Element.isOverflowWidth | ||||||
|  |     get() = scrollWidth > clientWidth | ||||||
|  |  | ||||||
|  | inline val Element.isOverflowHeight | ||||||
|  |     get() = scrollHeight > clientHeight | ||||||
|  |  | ||||||
|  | inline val Element.isOverflow | ||||||
|  |     get() = isOverflowHeight || isOverflowWidth | ||||||
| @@ -2,10 +2,12 @@ package dev.inmo.micro_utils.common | |||||||
|  |  | ||||||
| import org.khronos.webgl.ArrayBuffer | import org.khronos.webgl.ArrayBuffer | ||||||
| import org.w3c.dom.ErrorEvent | import org.w3c.dom.ErrorEvent | ||||||
| import org.w3c.files.File | import org.w3c.files.* | ||||||
| import org.w3c.files.FileReader |  | ||||||
| import kotlin.js.Promise | import kotlin.js.Promise | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @suppress | ||||||
|  |  */ | ||||||
| actual typealias MPPFile = File | actual typealias MPPFile = File | ||||||
|  |  | ||||||
| fun MPPFile.readBytesPromise() = Promise<ByteArray> { success, failure -> | fun MPPFile.readBytesPromise() = Promise<ByteArray> { success, failure -> | ||||||
| @@ -21,12 +23,32 @@ fun MPPFile.readBytesPromise() = Promise<ByteArray> { success, failure -> | |||||||
|     reader.readAsArrayBuffer(this) |     reader.readAsArrayBuffer(this) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | fun MPPFile.readBytes(): ByteArray { | ||||||
|  |     val reader = FileReaderSync() | ||||||
|  |     return reader.readAsArrayBuffer(this).toByteArray() | ||||||
|  | } | ||||||
|  |  | ||||||
| private suspend fun MPPFile.dirtyReadBytes(): ByteArray = readBytesPromise().await() | private suspend fun MPPFile.dirtyReadBytes(): ByteArray = readBytesPromise().await() | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @suppress | ||||||
|  |  */ | ||||||
| actual val MPPFile.filename: FileName | actual val MPPFile.filename: FileName | ||||||
|     get() = FileName(name) |     get() = FileName(name) | ||||||
|  | /** | ||||||
|  |  * @suppress | ||||||
|  |  */ | ||||||
| actual val MPPFile.filesize: Long | actual val MPPFile.filesize: Long | ||||||
|     get() = size.toLong() |     get() = size.toLong() | ||||||
|  | /** | ||||||
|  |  * @suppress | ||||||
|  |  */ | ||||||
|  | @Warning("That is not optimized version of bytes allocator. Use asyncBytesAllocator everywhere you can") | ||||||
|  | actual val MPPFile.bytesAllocatorSync: ByteArrayAllocator | ||||||
|  |     get() = ::readBytes | ||||||
|  | /** | ||||||
|  |  * @suppress | ||||||
|  |  */ | ||||||
| @Warning("That is not optimized version of bytes allocator. Use asyncBytesAllocator everywhere you can") | @Warning("That is not optimized version of bytes allocator. Use asyncBytesAllocator everywhere you can") | ||||||
| actual val MPPFile.bytesAllocator: SuspendByteArrayAllocator | actual val MPPFile.bytesAllocator: SuspendByteArrayAllocator | ||||||
|     get() = ::dirtyReadBytes |     get() = ::dirtyReadBytes | ||||||
|   | |||||||
| @@ -0,0 +1,38 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | import kotlinx.browser.document | ||||||
|  | import org.w3c.dom.* | ||||||
|  | import org.w3c.dom.events.Event | ||||||
|  | import org.w3c.dom.events.EventListener | ||||||
|  |  | ||||||
|  | fun Element.onActionOutside(type: String, options: dynamic = null, callback: (Event) -> Unit): EventListener { | ||||||
|  |     lateinit var observer: MutationObserver | ||||||
|  |     val listener = EventListener { | ||||||
|  |         val elementsToCheck = mutableListOf<Element>(this@onActionOutside) | ||||||
|  |         while (it.target != this@onActionOutside && elementsToCheck.isNotEmpty()) { | ||||||
|  |             val childrenGettingElement = elementsToCheck.removeFirst() | ||||||
|  |             for (i in 0 until childrenGettingElement.childElementCount) { | ||||||
|  |                 elementsToCheck.add(childrenGettingElement.children[i] ?: continue) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         if (elementsToCheck.isEmpty()) { | ||||||
|  |             callback(it) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     if (options == null) { | ||||||
|  |         document.addEventListener(type, listener) | ||||||
|  |     } else { | ||||||
|  |         document.addEventListener(type, listener, options) | ||||||
|  |     } | ||||||
|  |     observer = onRemoved { | ||||||
|  |         if (options == null) { | ||||||
|  |             document.removeEventListener(type, listener) | ||||||
|  |         } else { | ||||||
|  |             document.removeEventListener(type, listener, options) | ||||||
|  |         } | ||||||
|  |         observer.disconnect() | ||||||
|  |     } | ||||||
|  |     return listener | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fun Element.onClickOutside(options: dynamic = null, callback: (Event) -> Unit) = onActionOutside("click", options, callback) | ||||||
| @@ -0,0 +1,8 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | import kotlinx.browser.window | ||||||
|  |  | ||||||
|  | fun openLink(link: String, target: String = "_blank", features: String = "") { | ||||||
|  |     window.open(link, target, features) ?.focus() | ||||||
|  | } | ||||||
|  |  | ||||||
| @@ -0,0 +1,58 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | import org.w3c.dom.* | ||||||
|  | import kotlin.js.Json | ||||||
|  | import kotlin.js.json | ||||||
|  |  | ||||||
|  | external class ResizeObserver( | ||||||
|  |     callback: (Array<ResizeObserverEntry>, ResizeObserver) -> Unit | ||||||
|  | ) { | ||||||
|  |     fun observe(target: Element, options: Json = definedExternally) | ||||||
|  |  | ||||||
|  |     fun unobserve(target: Element) | ||||||
|  |  | ||||||
|  |     fun disconnect() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | external interface ResizeObserverSize { | ||||||
|  |     val blockSize: Float | ||||||
|  |     val inlineSize: Float | ||||||
|  | } | ||||||
|  |  | ||||||
|  | external interface ResizeObserverEntry { | ||||||
|  |     val borderBoxSize: Array<ResizeObserverSize> | ||||||
|  |     val contentBoxSize: Array<ResizeObserverSize> | ||||||
|  |     val devicePixelContentBoxSize: Array<ResizeObserverSize> | ||||||
|  |     val contentRect: DOMRectReadOnly | ||||||
|  |     val target: Element | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fun ResizeObserver.observe(target: Element, options: ResizeObserverObserveOptions) = observe( | ||||||
|  |     target, | ||||||
|  |     json( | ||||||
|  |         "box" to options.box ?.name | ||||||
|  |     ) | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | class ResizeObserverObserveOptions( | ||||||
|  |     val box: Box? = null | ||||||
|  | ) { | ||||||
|  |     sealed interface Box { | ||||||
|  |         val name: String | ||||||
|  |  | ||||||
|  |         object Content : Box { | ||||||
|  |             override val name: String | ||||||
|  |                 get() = "content-box" | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         object Border : Box { | ||||||
|  |             override val name: String | ||||||
|  |                 get() = "border-box" | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         object DevicePixelContent : Box { | ||||||
|  |             override val name: String | ||||||
|  |                 get() = "device-pixel-content-box" | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,30 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | import kotlinx.browser.document | ||||||
|  | import kotlinx.dom.createElement | ||||||
|  | import org.w3c.dom.HTMLElement | ||||||
|  | import org.w3c.dom.HTMLInputElement | ||||||
|  | import org.w3c.files.get | ||||||
|  |  | ||||||
|  | fun selectFile( | ||||||
|  |     inputSetup: (HTMLInputElement) -> Unit = {}, | ||||||
|  |     onFailure: (Throwable) -> Unit = {}, | ||||||
|  |     onFile: (MPPFile) -> Unit | ||||||
|  | ) { | ||||||
|  |     (document.createElement("input") { | ||||||
|  |         (this as HTMLInputElement).apply { | ||||||
|  |             type = "file" | ||||||
|  |             onchange = { | ||||||
|  |                 runCatching { | ||||||
|  |                     files ?.get(0) ?: error("File must not be null") | ||||||
|  |                 }.onSuccess { | ||||||
|  |                     onFile(it) | ||||||
|  |                 }.onFailure { | ||||||
|  |                     onFailure(it) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             inputSetup(this) | ||||||
|  |         } | ||||||
|  |     } as HTMLElement).click() | ||||||
|  | } | ||||||
|  |  | ||||||
| @@ -0,0 +1,14 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | import kotlinx.browser.document | ||||||
|  | import org.w3c.dom.HTMLAnchorElement | ||||||
|  |  | ||||||
|  | fun triggerDownloadFile(filename: String, fileLink: String) { | ||||||
|  |     val hiddenElement = document.createElement("a") as HTMLAnchorElement | ||||||
|  |  | ||||||
|  |     hiddenElement.href = fileLink | ||||||
|  |     hiddenElement.target = "_blank" | ||||||
|  |     hiddenElement.download = filename | ||||||
|  |     hiddenElement.click() | ||||||
|  | } | ||||||
|  |  | ||||||
| @@ -0,0 +1,4 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | actual fun Float.fixed(signs: Int): Float = this.asDynamic().toFixed(signs.coerceIn(FixedSignsRange)).unsafeCast<String>().toFloat() | ||||||
|  | actual fun Double.fixed(signs: Int): Double = this.asDynamic().toFixed(signs.coerceIn(FixedSignsRange)).unsafeCast<String>().toDouble() | ||||||
| @@ -0,0 +1,20 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | import java.io.File | ||||||
|  | import java.io.InputStream | ||||||
|  | import java.util.UUID | ||||||
|  |  | ||||||
|  | fun InputStream.downloadToTempFile( | ||||||
|  |     fileName: String = UUID.randomUUID().toString(), | ||||||
|  |     fileExtension: String? = ".temp", | ||||||
|  |     folder: File? = null | ||||||
|  | ) = File.createTempFile( | ||||||
|  |     fileName, | ||||||
|  |     fileExtension, | ||||||
|  |     folder | ||||||
|  | ).apply { | ||||||
|  |     outputStream().use { | ||||||
|  |         copyTo(it) | ||||||
|  |     } | ||||||
|  |     deleteOnExit() | ||||||
|  | } | ||||||
| @@ -4,12 +4,29 @@ import dev.inmo.micro_utils.coroutines.doInIO | |||||||
| import dev.inmo.micro_utils.coroutines.doOutsideOfCoroutine | import dev.inmo.micro_utils.coroutines.doOutsideOfCoroutine | ||||||
| import java.io.File | import java.io.File | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @suppress | ||||||
|  |  */ | ||||||
| actual typealias MPPFile = File | actual typealias MPPFile = File | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @suppress | ||||||
|  |  */ | ||||||
| actual val MPPFile.filename: FileName | actual val MPPFile.filename: FileName | ||||||
|     get() = FileName(name) |     get() = FileName(name) | ||||||
|  | /** | ||||||
|  |  * @suppress | ||||||
|  |  */ | ||||||
| actual val MPPFile.filesize: Long | actual val MPPFile.filesize: Long | ||||||
|     get() = length() |     get() = length() | ||||||
|  | /** | ||||||
|  |  * @suppress | ||||||
|  |  */ | ||||||
|  | actual val MPPFile.bytesAllocatorSync: ByteArrayAllocator | ||||||
|  |     get() = ::readBytes | ||||||
|  | /** | ||||||
|  |  * @suppress | ||||||
|  |  */ | ||||||
| actual val MPPFile.bytesAllocator: SuspendByteArrayAllocator | actual val MPPFile.bytesAllocator: SuspendByteArrayAllocator | ||||||
|     get() = { |     get() = { | ||||||
|         doInIO { |         doInIO { | ||||||
|   | |||||||
| @@ -0,0 +1,12 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | import java.math.BigDecimal | ||||||
|  | import java.math.RoundingMode | ||||||
|  |  | ||||||
|  | actual fun Float.fixed(signs: Int): Float = BigDecimal.valueOf(this.toDouble()) | ||||||
|  |     .setScale(signs.coerceIn(FixedSignsRange), RoundingMode.HALF_UP) | ||||||
|  |     .toFloat(); | ||||||
|  |  | ||||||
|  | actual fun Double.fixed(signs: Int): Double = BigDecimal.valueOf(this) | ||||||
|  |     .setScale(signs.coerceIn(FixedSignsRange), RoundingMode.HALF_UP) | ||||||
|  |     .toDouble(); | ||||||
							
								
								
									
										36
									
								
								common/src/linuxX64Main/kotlin/ActualMPPFile.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								common/src/linuxX64Main/kotlin/ActualMPPFile.kt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,36 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | import okio.FileSystem | ||||||
|  | import okio.Path | ||||||
|  | import okio.use | ||||||
|  |  | ||||||
|  | actual typealias MPPFile = Path | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @suppress | ||||||
|  |  */ | ||||||
|  | actual val MPPFile.filename: FileName | ||||||
|  |     get() = FileName(toString()) | ||||||
|  | /** | ||||||
|  |  * @suppress | ||||||
|  |  */ | ||||||
|  | actual val MPPFile.filesize: Long | ||||||
|  |     get() = FileSystem.SYSTEM.openReadOnly(this).use { | ||||||
|  |         it.size() | ||||||
|  |     } | ||||||
|  | /** | ||||||
|  |  * @suppress | ||||||
|  |  */ | ||||||
|  | actual val MPPFile.bytesAllocatorSync: ByteArrayAllocator | ||||||
|  |     get() = { | ||||||
|  |         FileSystem.SYSTEM.read(this) { | ||||||
|  |             readByteArray() | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | /** | ||||||
|  |  * @suppress | ||||||
|  |  */ | ||||||
|  | actual val MPPFile.bytesAllocator: SuspendByteArrayAllocator | ||||||
|  |     get() = { | ||||||
|  |         bytesAllocatorSync() | ||||||
|  |     } | ||||||
							
								
								
									
										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() | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										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() | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -10,13 +10,19 @@ kotlin { | |||||||
|     sourceSets { |     sourceSets { | ||||||
|         commonMain { |         commonMain { | ||||||
|             dependencies { |             dependencies { | ||||||
|                 api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version" |                 api libs.kt.coroutines | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         jsMain { | ||||||
|  |             dependencies { | ||||||
|  |                 api project(":micro_utils.common") | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         androidMain { |         androidMain { | ||||||
|             dependencies { |             dependencies { | ||||||
|                 api "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version" |                 api libs.kt.coroutines.android | ||||||
|             } |             } | ||||||
|  |             dependsOn(jvmMain) | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										20
									
								
								coroutines/compose/build.gradle
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								coroutines/compose/build.gradle
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | |||||||
|  | plugins { | ||||||
|  |     id "org.jetbrains.kotlin.multiplatform" | ||||||
|  |     id "org.jetbrains.kotlin.plugin.serialization" | ||||||
|  |     id "com.android.library" | ||||||
|  |     alias(libs.plugins.jb.compose) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | apply from: "$mppProjectWithSerializationAndComposePresetPath" | ||||||
|  |  | ||||||
|  | kotlin { | ||||||
|  |     sourceSets { | ||||||
|  |         commonMain { | ||||||
|  |             dependencies { | ||||||
|  |                 api libs.kt.coroutines | ||||||
|  |                 api project(":micro_utils.coroutines") | ||||||
|  |                 api project(":micro_utils.common.compose") | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										1
									
								
								coroutines/compose/src/androidMain/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								coroutines/compose/src/androidMain/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | <manifest package="dev.inmo.micro_utils.coroutines.compose"/> | ||||||
| @@ -0,0 +1,60 @@ | |||||||
|  | package dev.inmo.micro_utils.coroutines.compose | ||||||
|  |  | ||||||
|  | import androidx.compose.runtime.* | ||||||
|  | import androidx.compose.runtime.snapshots.SnapshotStateList | ||||||
|  | import dev.inmo.micro_utils.common.applyDiff | ||||||
|  | import dev.inmo.micro_utils.coroutines.ExceptionHandler | ||||||
|  | import dev.inmo.micro_utils.coroutines.defaultSafelyWithoutExceptionHandlerWithNull | ||||||
|  | import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions | ||||||
|  | import kotlinx.coroutines.CoroutineScope | ||||||
|  | import kotlinx.coroutines.Dispatchers | ||||||
|  | import kotlinx.coroutines.flow.Flow | ||||||
|  | import kotlinx.coroutines.flow.StateFlow | ||||||
|  | import kotlinx.coroutines.withContext | ||||||
|  | import kotlin.coroutines.CoroutineContext | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Each value of [this] [Flow] will trigger [applyDiff] to the result [SnapshotStateList] | ||||||
|  |  * | ||||||
|  |  * @param scope Will be used to [subscribeSafelyWithoutExceptions] on [this] to update returned [SnapshotStateList] | ||||||
|  |  * @param useContextOnChange Will be used to change context inside of [subscribeSafelyWithoutExceptions] to ensure that | ||||||
|  |  * change will happen in the required [CoroutineContext]. [Dispatchers.Main] by default | ||||||
|  |  * @param onException Will be passed to the [subscribeSafelyWithoutExceptions] as uncaught exceptions handler | ||||||
|  |  */ | ||||||
|  | @Suppress("NOTHING_TO_INLINE") | ||||||
|  | inline fun <reified T> Flow<List<T>>.asMutableComposeListState( | ||||||
|  |     scope: CoroutineScope, | ||||||
|  |     useContextOnChange: CoroutineContext? = Dispatchers.Main, | ||||||
|  |     noinline onException: ExceptionHandler<List<T>?> = defaultSafelyWithoutExceptionHandlerWithNull, | ||||||
|  | ): SnapshotStateList<T> { | ||||||
|  |     val state = mutableStateListOf<T>() | ||||||
|  |     val changeBlock: suspend (List<T>) -> Unit = useContextOnChange ?.let { | ||||||
|  |         { | ||||||
|  |             withContext(useContextOnChange) { | ||||||
|  |                 state.applyDiff(it) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } ?: { | ||||||
|  |         state.applyDiff(it) | ||||||
|  |     } | ||||||
|  |     subscribeSafelyWithoutExceptions(scope, onException, changeBlock) | ||||||
|  |     return state | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * In fact, it is just classcast of [asMutableComposeListState] to [List] | ||||||
|  |  * | ||||||
|  |  * @param scope Will be used to [subscribeSafelyWithoutExceptions] on [this] to update returned [List] | ||||||
|  |  * @param useContextOnChange Will be used to change context inside of [subscribeSafelyWithoutExceptions] to ensure that | ||||||
|  |  * change will happen in the required [CoroutineContext]. [Dispatchers.Main] by default | ||||||
|  |  * @param onException Will be passed to the [subscribeSafelyWithoutExceptions] as uncaught exceptions handler | ||||||
|  |  * | ||||||
|  |  * @return Changing in time [List] which follow [Flow] values | ||||||
|  |  */ | ||||||
|  | @Suppress("NOTHING_TO_INLINE") | ||||||
|  | inline fun <reified T> Flow<List<T>>.asComposeList( | ||||||
|  |     scope: CoroutineScope, | ||||||
|  |     useContextOnChange: CoroutineContext? = Dispatchers.Main, | ||||||
|  |     noinline onException: ExceptionHandler<List<T>?> = defaultSafelyWithoutExceptionHandlerWithNull, | ||||||
|  | ): List<T> = asMutableComposeListState(scope, useContextOnChange, onException) | ||||||
|  |  | ||||||
| @@ -0,0 +1,94 @@ | |||||||
|  | package dev.inmo.micro_utils.coroutines.compose | ||||||
|  |  | ||||||
|  | import androidx.compose.runtime.* | ||||||
|  | import dev.inmo.micro_utils.common.compose.asState | ||||||
|  | import dev.inmo.micro_utils.coroutines.ExceptionHandler | ||||||
|  | import dev.inmo.micro_utils.coroutines.defaultSafelyWithoutExceptionHandlerWithNull | ||||||
|  | import dev.inmo.micro_utils.coroutines.doInUI | ||||||
|  | import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions | ||||||
|  | import kotlinx.coroutines.CoroutineScope | ||||||
|  | import kotlinx.coroutines.Dispatchers | ||||||
|  | import kotlinx.coroutines.flow.Flow | ||||||
|  | import kotlinx.coroutines.flow.StateFlow | ||||||
|  | import kotlinx.coroutines.withContext | ||||||
|  | import kotlin.coroutines.CoroutineContext | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Will map [this] [Flow] as [MutableState]. Returned [MutableState] WILL NOT change source [Flow] | ||||||
|  |  * | ||||||
|  |  * @param initial First value which will be passed to the result [MutableState] | ||||||
|  |  * @param scope Will be used to [subscribeSafelyWithoutExceptions] on [this] to update returned [MutableState] | ||||||
|  |  * @param useContextOnChange Will be used to change context inside of [subscribeSafelyWithoutExceptions] to ensure that | ||||||
|  |  * change will happen in the required [CoroutineContext]. [Dispatchers.Main] by default | ||||||
|  |  * @param onException Will be passed to the [subscribeSafelyWithoutExceptions] as uncaught exceptions handler | ||||||
|  |  */ | ||||||
|  | fun <T> Flow<T>.asMutableComposeState( | ||||||
|  |     initial: T, | ||||||
|  |     scope: CoroutineScope, | ||||||
|  |     useContextOnChange: CoroutineContext? = Dispatchers.Main, | ||||||
|  |     onException: ExceptionHandler<T?> = defaultSafelyWithoutExceptionHandlerWithNull, | ||||||
|  | ): MutableState<T> { | ||||||
|  |     val state = mutableStateOf(initial) | ||||||
|  |     val changeBlock: suspend (T) -> Unit = useContextOnChange ?.let { | ||||||
|  |         { | ||||||
|  |             withContext(useContextOnChange) { | ||||||
|  |                 state.value = it | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } ?: { | ||||||
|  |         state.value = it | ||||||
|  |     } | ||||||
|  |     subscribeSafelyWithoutExceptions(scope, onException, block = changeBlock) | ||||||
|  |     return state | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Will map [this] [StateFlow] as [MutableState]. Returned [MutableState] WILL NOT change source [StateFlow]. | ||||||
|  |  * This conversation will pass its [StateFlow.value] as the first value | ||||||
|  |  * | ||||||
|  |  * @param scope Will be used to [subscribeSafelyWithoutExceptions] on [this] to update returned [MutableState] | ||||||
|  |  * @param useContextOnChange Will be used to change context inside of [subscribeSafelyWithoutExceptions] to ensure that | ||||||
|  |  * change will happen in the required [CoroutineContext]. [Dispatchers.Main] by default | ||||||
|  |  * @param onException Will be passed to the [subscribeSafelyWithoutExceptions] as uncaught exceptions handler | ||||||
|  |  */ | ||||||
|  | @Suppress("NOTHING_TO_INLINE") | ||||||
|  | inline fun <T> StateFlow<T>.asMutableComposeState( | ||||||
|  |     scope: CoroutineScope, | ||||||
|  |     useContextOnChange: CoroutineContext? = Dispatchers.Main, | ||||||
|  |     noinline onException: ExceptionHandler<T?> = defaultSafelyWithoutExceptionHandlerWithNull, | ||||||
|  | ): MutableState<T> = asMutableComposeState(value, scope, useContextOnChange, onException) | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Will create [MutableState] using [asMutableComposeState] and use [asState] to convert it as immutable state | ||||||
|  |  * | ||||||
|  |  * @param initial First value which will be passed to the result [State] | ||||||
|  |  * @param scope Will be used to [subscribeSafelyWithoutExceptions] on [this] to update returned [State] | ||||||
|  |  * @param useContextOnChange Will be used to change context inside of [subscribeSafelyWithoutExceptions] to ensure that | ||||||
|  |  * change will happen in the required [CoroutineContext]. [Dispatchers.Main] by default | ||||||
|  |  * @param onException Will be passed to the [subscribeSafelyWithoutExceptions] as uncaught exceptions handler | ||||||
|  |  */ | ||||||
|  | fun <T> Flow<T>.asComposeState( | ||||||
|  |     initial: T, | ||||||
|  |     scope: CoroutineScope, | ||||||
|  |     useContextOnChange: CoroutineContext? = Dispatchers.Main, | ||||||
|  |     onException: ExceptionHandler<T?> = defaultSafelyWithoutExceptionHandlerWithNull, | ||||||
|  | ): State<T> { | ||||||
|  |     val state = asMutableComposeState(initial, scope, useContextOnChange, onException) | ||||||
|  |     return state.asState() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Will map [this] [StateFlow] as [State]. This conversation will pass its [StateFlow.value] as the first value | ||||||
|  |  * | ||||||
|  |  * @param scope Will be used to [subscribeSafelyWithoutExceptions] on [this] to update returned [State] | ||||||
|  |  * @param useContextOnChange Will be used to change context inside of [subscribeSafelyWithoutExceptions] to ensure that | ||||||
|  |  * change will happen in the required [CoroutineContext]. [Dispatchers.Main] by default | ||||||
|  |  * @param onException Will be passed to the [subscribeSafelyWithoutExceptions] as uncaught exceptions handler | ||||||
|  |  */ | ||||||
|  | @Suppress("NOTHING_TO_INLINE") | ||||||
|  | inline fun <T> StateFlow<T>.asComposeState( | ||||||
|  |     scope: CoroutineScope, | ||||||
|  |     useContextOnChange: CoroutineContext? = Dispatchers.Main, | ||||||
|  |     noinline onException: ExceptionHandler<T?> = defaultSafelyWithoutExceptionHandlerWithNull, | ||||||
|  | ): State<T> = asComposeState(value, scope, useContextOnChange, onException) | ||||||
|  |  | ||||||
| @@ -0,0 +1,14 @@ | |||||||
|  | package dev.inmo.micro_utils.coroutines.compose | ||||||
|  |  | ||||||
|  | import androidx.compose.runtime.* | ||||||
|  | import kotlinx.coroutines.Job | ||||||
|  | import kotlinx.coroutines.job | ||||||
|  | import kotlin.coroutines.CoroutineContext | ||||||
|  |  | ||||||
|  | fun Composition.linkWithJob(job: Job) { | ||||||
|  |     job.invokeOnCompletion { | ||||||
|  |         this@linkWithJob.dispose() | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fun Composition.linkWithContext(coroutineContext: CoroutineContext) = linkWithJob(coroutineContext.job) | ||||||
| @@ -0,0 +1,26 @@ | |||||||
|  | package dev.inmo.micro_utils.coroutines.compose | ||||||
|  |  | ||||||
|  | import androidx.compose.runtime.* | ||||||
|  | import dev.inmo.micro_utils.common.compose.linkWithElement | ||||||
|  | import kotlinx.coroutines.* | ||||||
|  | import org.jetbrains.compose.web.dom.DOMScope | ||||||
|  | import org.w3c.dom.Element | ||||||
|  |  | ||||||
|  | suspend fun <TElement : Element> renderComposableAndLinkToContext( | ||||||
|  |     root: TElement, | ||||||
|  |     monotonicFrameClock: MonotonicFrameClock = DefaultMonotonicFrameClock, | ||||||
|  |     content: @Composable DOMScope<TElement>.() -> Unit | ||||||
|  | ): Composition = org.jetbrains.compose.web.renderComposable(root, monotonicFrameClock, content).apply { | ||||||
|  |     linkWithContext( | ||||||
|  |         currentCoroutineContext() | ||||||
|  |     ) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | suspend fun <TElement : Element> renderComposableAndLinkToContextAndRoot( | ||||||
|  |     root: TElement, | ||||||
|  |     monotonicFrameClock: MonotonicFrameClock = DefaultMonotonicFrameClock, | ||||||
|  |     content: @Composable DOMScope<TElement>.() -> Unit | ||||||
|  | ): Composition = org.jetbrains.compose.web.renderComposable(root, monotonicFrameClock, content).apply { | ||||||
|  |     linkWithContext(currentCoroutineContext()) | ||||||
|  |     linkWithElement(root) | ||||||
|  | } | ||||||
| @@ -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) | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,100 @@ | |||||||
|  | package dev.inmo.micro_utils.coroutines | ||||||
|  |  | ||||||
|  | import kotlinx.coroutines.CoroutineScope | ||||||
|  | import kotlinx.coroutines.channels.BufferOverflow | ||||||
|  | import kotlinx.coroutines.channels.Channel | ||||||
|  | import kotlinx.coroutines.flow.* | ||||||
|  | import kotlinx.coroutines.sync.Mutex | ||||||
|  | import kotlinx.coroutines.sync.withLock | ||||||
|  | import kotlin.coroutines.cancellation.CancellationException | ||||||
|  |  | ||||||
|  | private sealed interface AccumulatorFlowStep<T> | ||||||
|  | private data class DataRetrievedAccumulatorFlowStep<T>(val data: T) : AccumulatorFlowStep<T> | ||||||
|  | private data class SubscribeAccumulatorFlowStep<T>(val channel: Channel<T>) : AccumulatorFlowStep<T> | ||||||
|  | private data class UnsubscribeAccumulatorFlowStep<T>(val channel: Channel<T>) : AccumulatorFlowStep<T> | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * This [Flow] will have behaviour very similar to [SharedFlow], but there are several differences: | ||||||
|  |  * | ||||||
|  |  * * All unhandled by [FlowCollector] data will not be removed from [AccumulatorFlow] and will be sent to new | ||||||
|  |  * [FlowCollector]s until anybody will handle it | ||||||
|  |  * * Here there are an [activeData] where data [T] will be stored until somebody will handle it | ||||||
|  |  */ | ||||||
|  | class AccumulatorFlow<T>( | ||||||
|  |     sourceDataFlow: Flow<T>, | ||||||
|  |     scope: CoroutineScope | ||||||
|  | ) : AbstractFlow<T>() { | ||||||
|  |     private val subscope = scope.LinkedSupervisorScope() | ||||||
|  |     private val activeData = ArrayDeque<T>() | ||||||
|  |     private val dataMutex = Mutex() | ||||||
|  |     private val channelsForBroadcast = mutableListOf<Channel<T>>() | ||||||
|  |     private val channelsMutex = Mutex() | ||||||
|  |     private val steps = subscope.actor<AccumulatorFlowStep<T>> { step -> | ||||||
|  |         when (step) { | ||||||
|  |             is DataRetrievedAccumulatorFlowStep -> { | ||||||
|  |                 if (activeData.firstOrNull() === step.data) { | ||||||
|  |                     dataMutex.withLock { | ||||||
|  |                         activeData.removeFirst() | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             is SubscribeAccumulatorFlowStep -> channelsMutex.withLock { | ||||||
|  |                 channelsForBroadcast.add(step.channel) | ||||||
|  |                 dataMutex.withLock { | ||||||
|  |                     val dataToSend = activeData.toList() | ||||||
|  |                     safelyWithoutExceptions { | ||||||
|  |                         dataToSend.forEach { step.channel.send(it) } | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             is UnsubscribeAccumulatorFlowStep -> channelsMutex.withLock { | ||||||
|  |                 channelsForBroadcast.remove(step.channel) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     private val subscriptionJob = sourceDataFlow.subscribeSafelyWithoutExceptions(subscope) { | ||||||
|  |         dataMutex.withLock { | ||||||
|  |             activeData.addLast(it) | ||||||
|  |         } | ||||||
|  |         channelsMutex.withLock { | ||||||
|  |             channelsForBroadcast.forEach { channel -> | ||||||
|  |                 safelyWithResult { | ||||||
|  |                     channel.send(it) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override suspend fun collectSafely(collector: FlowCollector<T>) { | ||||||
|  |         val channel = Channel<T>(Channel.UNLIMITED, BufferOverflow.SUSPEND) | ||||||
|  |         steps.send(SubscribeAccumulatorFlowStep(channel)) | ||||||
|  |         val result = runCatchingSafely { | ||||||
|  |             for (data in channel) { | ||||||
|  |                 val emitResult = runCatchingSafely { | ||||||
|  |                     collector.emit(data) | ||||||
|  |                 } | ||||||
|  |                 if (emitResult.isSuccess || emitResult.exceptionOrNull() is CancellationException) { | ||||||
|  |                     steps.send(DataRetrievedAccumulatorFlowStep(data)) | ||||||
|  |                 } | ||||||
|  |                 emitResult.getOrThrow() | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         channel.cancel() | ||||||
|  |         steps.send(UnsubscribeAccumulatorFlowStep(channel)) | ||||||
|  |         result.getOrThrow() | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Creates [AccumulatorFlow] using [this] as base [Flow] | ||||||
|  |  */ | ||||||
|  | fun <T> Flow<T>.accumulatorFlow(scope: CoroutineScope): Flow<T> { | ||||||
|  |     return AccumulatorFlow(this, scope) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Creates [AccumulatorFlow] using [this] with [receiveAsFlow] to get | ||||||
|  |  */ | ||||||
|  | fun <T> Channel<T>.accumulatorFlow(scope: CoroutineScope): Flow<T> { | ||||||
|  |     return receiveAsFlow().accumulatorFlow(scope) | ||||||
|  | } | ||||||
| @@ -1,19 +1,15 @@ | |||||||
| package dev.inmo.micro_utils.coroutines | 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() | ||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user