mirror of
				https://github.com/InsanusMokrassar/MicroUtils.git
				synced 2025-10-31 04:05:32 +00:00 
			
		
		
		
	Compare commits
	
		
			1009 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 438fefa7a3 | |||
| 5d74eac814 | |||
| 9fb62e1e25 | |||
| 3e366ea73b | |||
| 0ff895bffa | |||
| c5bb120280 | |||
| 4b26a92b37 | |||
| 0a8453b4d2 | |||
| c9872a61b6 | |||
| 149a1aa278 | |||
| 13d0e1b682 | |||
| 6f9be2a9f8 | |||
| 93ba98d993 | |||
| 940ee3df06 | |||
| 2e7bab10fd | |||
|  | 3ed70a37ea | ||
| fe8f80b9d9 | |||
| d81fb32fb9 | |||
| 2877b5532c | |||
| b938b21395 | |||
| 58836359cc | |||
| 5edb0e1331 | |||
| 0f0d0b5d58 | |||
| 46c1887cbe | |||
| 5f231c2212 | |||
| 4e97ce86aa | |||
| 315a7cb29e | |||
| aa7cc503f2 | |||
| 4bbe7e5a80 | |||
| d9c05f38d2 | |||
| cd0c4c9650 | |||
| fc3407f104 | |||
| 3a5544206b | |||
| e17e2f7fb8 | |||
|  | d32c95f143 | ||
| 6d8a8ab018 | |||
| a7dce8fa78 | |||
| ca73ff8e19 | |||
| d01ad10d7d | |||
| 81041ee43c | |||
|  | 6e004c2ae4 | ||
| 0e2fac5b22 | |||
| 269da7f155 | |||
| 3cb6b73ee0 | |||
| a938ee1efb | |||
| 6ea5e2e5a6 | |||
| 617dfb54e0 | |||
| d23e005985 | |||
| e5207f5bc5 | |||
| c96cea8db0 | |||
| 0a8e71d76a | |||
| cf1fd32b08 | |||
| cc4224ea1f | |||
| f4c148bc58 | |||
| 022297ad3f | |||
| 5180d6fc3e | |||
| eeebbff70d | |||
| afc6aeea15 | |||
| 486515eddd | |||
| 0e21699cd1 | |||
| f1678ef7cf | |||
| cea65fc76e | |||
| c9e320b72a | |||
| 555956087d | |||
| b3f468f901 | |||
| f5f7511781 | |||
| 4be1d93f60 | |||
| 7d684608ef | |||
| 2c7fd320eb | |||
| 88ee82e1c6 | |||
| d6402c624e | |||
| 8b9c93bc10 | |||
| 4f5e261d01 | |||
| cf455aebe6 | |||
| 1380d5f8e1 | |||
| 5f65260bfe | |||
| 11f0dcfc01 | |||
| 555b7886a4 | |||
| 3707a6c0ce | |||
| 4c8d92b4c3 | |||
| 8bbd33c896 | |||
| ac33a3580f | |||
| a64a32fbe6 | |||
| 9493e97a38 | |||
| 88bd770260 | |||
| a7bd33b7bf | |||
| 73c724a2e5 | |||
| d8cf3c6726 | |||
| 15dee238b5 | |||
| c70626734e | |||
| 5a765ea1bc | |||
| 8215f9d2c6 | |||
| d2e6d2ec80 | |||
| 3718181e8f | |||
| 0d825cf424 | |||
| 28a804d988 | |||
| 9e4bb9d678 | |||
| 9c40d7da3d | |||
| 2b76ad0aa9 | |||
| e4b619e050 | |||
| 36c09feaf2 | |||
| 2d68321503 | |||
| 85455ab21c | |||
| 18d63eb980 | |||
| 2e429e9704 | |||
| f4af28059b | |||
| c1476bd075 | |||
| 16c720fddd | |||
| 8b4b4a5eca | |||
| 32e6e5b7e2 | |||
| a9f7fd8e32 | |||
| 95be1a26f2 | |||
| ef9b31aee0 | |||
| df3c01ff0a | |||
| 4704c5a33d | |||
| 225c06550a | |||
| f0987614c6 | |||
| 269c2876f3 | |||
| 168d6acf7c | |||
| a5f718e257 | |||
| 4f68459582 | |||
| 442db122cf | |||
| 580d757be2 | |||
| 47b0f6d2d8 | |||
| 3f6f6ebc2b | |||
| 2645ea29d6 | |||
| 79f2041565 | |||
| 4a7567f288 | |||
| 8a890ed6ed | |||
| 3d90df6897 | |||
| 681c13144a | |||
| b64f2e6d32 | |||
| 428eabb1bd | |||
| 2162e83bce | |||
| 6142022283 | |||
| e6d9c8250f | |||
| 46178e723b | |||
| 605f55acd2 | |||
| 0f8b69aa60 | |||
| 551d8ec480 | |||
| fc48446ec4 | |||
| 3644b83ac6 | |||
| cd73791b6f | |||
| 03de71df2e | |||
| 83d5d3faf4 | |||
| 0c8bec4c89 | |||
| 7fc93817c1 | |||
| d0a00031a1 | |||
| 6ebc5aa0c2 | |||
| 8a6b4bb49e | |||
| 20799b9a3e | |||
| ec3afc615c | |||
| da692ccfc3 | |||
| 53b89f3a18 | |||
| 58cded28d3 | |||
| 592c5f3732 | |||
| f44a78a5f5 | |||
| e0bdd5dfdc | |||
| 99c0f06b72 | |||
| 66fc6df3d7 | |||
| a36425a905 | |||
| d920fee6d4 | |||
| 23590be5de | |||
| 94acc3c93b | |||
| 5616326a3b | |||
| 7601860c5c | |||
| 8b43d785cc | |||
| b62d3a0b7d | |||
| fad73c7213 | |||
| 2403c7c2b0 | |||
| fa090bf920 | |||
| a83ee86340 | |||
| 204955bcce | |||
| ee56e9543a | |||
| 96fdff6ffd | |||
| 58b007cbb3 | |||
| 4f0c139889 | |||
| c584c24fce | |||
| 85e5cee154 | |||
| 5af91981f1 | |||
| 2fe4f08059 | |||
| 83796f345a | |||
| c1e21364a6 | |||
| 067d9d0d3b | |||
| 03f527d83e | |||
| ced05a4586 | |||
| 43fe06206a | |||
| 023657558e | |||
| 9b0b726c80 | |||
| 4ee67321c4 | |||
| 59f1f2e59b | |||
| 0766d48b7c | |||
| e18903b9e9 | |||
| d0eecdead2 | |||
| cc4a83a033 | |||
| 1cf911bbde | |||
| 36d73d5023 | |||
| c395242e3e | |||
| cd9cd7cc5d | |||
| acbb8a0c07 | |||
| b9d8528599 | |||
| 4971326eca | |||
| 09d1047260 | |||
| 02dbd493c2 | |||
| b17931e7bd | |||
| 2a4570eafc | |||
| c9514d3a6d | |||
| 072805efc7 | |||
| 369ff26627 | |||
| c5abbbbd2d | |||
| d974639f1e | |||
| 26efde316b | |||
| fafe50f80a | |||
| 41504469db | |||
| 03b3ddd98b | |||
| 89d919f2be | |||
| b53cfd5504 | |||
| 31022733ac | |||
| f9a8c39879 | |||
| a812c2dd2f | |||
| 217e977f0d | |||
| 04c301d1ac | |||
| 7f0c425389 | |||
| 1ede1c423b | |||
| 7cb064896a | |||
| 0c5e2862ca | |||
| 30d4507f54 | |||
| b6c6d89455 | |||
| 9d218ee534 | |||
| 11116d8cab | |||
| 58d754bbde | |||
| 8f25c123dd | |||
| 76e214fc08 | |||
| 2b5380f8d6 | |||
| 844a129094 | |||
| a3090cca9d | |||
| b7b5159e9c | |||
| 0f8bc2c950 | |||
| 69f5c49f45 | |||
| 9b308e6fb8 | |||
| 3e3f91128b | |||
| 0d1aae0ef7 | |||
| 4d022f0480 | |||
| 153e20d00e | |||
| a9a8171dd6 | |||
| bf5c3b59b2 | |||
| 607c432bdb | |||
| ae5c010770 | |||
| d5e432437f | |||
| 9f99ebce01 | |||
| 64ee899b84 | |||
| e0e0c1658b | |||
| 2c586f667c | |||
| 64164ef6c1 | |||
| 22343c0731 | |||
| f4ec1a4c60 | |||
| c1c33cceb1 | |||
| a3e975b2ba | |||
| 06e705a687 | |||
| d56eb6c867 | |||
| 9cbca864e3 | |||
| abb4378694 | |||
| 0eb698d9a4 | |||
| 15ea9f2093 | |||
| d47aca0923 | |||
| 1ac50e9959 | |||
| 6adfbe3a96 | |||
| 59f36e62e9 | |||
| 54af116009 | |||
| 38fbec8e3b | |||
| babbfc55e4 | |||
| 2511e18d69 | |||
| 29658c70a0 | |||
| 96311ee43d | |||
| bd33b09052 | |||
| 8055003b47 | |||
| 1257492f85 | |||
| 1107b7f4ef | |||
| a1a1171240 | |||
| 46c02e5df1 | |||
| 2e9efc57de | |||
| acecadef17 | |||
| 19394b5e69 | |||
| de999e197f | |||
| 9d95687d3c | |||
| aa9dfb4ab8 | |||
| 9c5b44efb3 | |||
| ac587a67e6 | |||
| 59428140a8 | |||
| 60bdb59d71 | |||
| be52871de8 | |||
| b7934cf357 | |||
| dbfbeef90a | |||
| 00943c9cdf | |||
| 8745c6a16a | |||
| 433ba4b58f | |||
| d40376e524 | |||
| a2982f88f5 | |||
| 1642f7abd9 | |||
| a10d2184ff | |||
| 522435f096 | |||
| 79b30290c0 | |||
| f8b8626859 | |||
| b061b85a08 | |||
| 3870db1c88 | |||
| 1be1070eb4 | |||
| 2696e663cf | |||
| 1e1f7df86d | |||
| 1d8ded8fd3 | |||
| 197825123a | |||
| 422b2e6db1 | |||
| 1973e0b5bf | |||
| 8258cf93a9 | |||
| 1d49bd5947 | |||
| 44317d1519 | |||
| 48e08fcc69 | |||
| 1a3ce6e623 | |||
| fb122f3e70 | |||
| 7cca6039cc | |||
| 118e3dba39 | |||
| 87070710fa | |||
| 38499c3d4a | |||
| 5d754d968b | |||
| d543d436bc | |||
| 12b54f99af | |||
| 2a95d7e643 | |||
| e3d3cacfa4 | |||
| 4b13491a0e | |||
| 85d516d1e9 | |||
| ac58b6a7e3 | |||
| 2cc6126765 | |||
| f94b085850 | |||
| c9822a491b | |||
| 23b2d60295 | |||
| f4bc9eed39 | |||
| e310f188b0 | |||
| 6c1571188c | |||
| 945d2fa284 | |||
| 020095f1ff | |||
| b165a76e62 | |||
| 03c8830672 | |||
| 38448da89b | |||
| 2ade5aff91 | |||
| 29bf6e80ec | |||
| 027e927e1b | |||
| fa061f88e2 | |||
| 1d01b65b5f | |||
| 2c2b364167 | |||
| 7c5fc9bf7c | |||
| 193d22ff20 | |||
| 0f3b553ba2 | |||
| 4eb1013446 | |||
| be1b2563ec | |||
| 1c9c2f1e70 | |||
| 52dfded741 | |||
| 0a115e5cf4 | |||
| c349af999b | |||
| 9a5709a34d | |||
| 28d311160b | |||
| 1ca7081a40 | |||
| 5ce8ebe82c | |||
| a7a88b29b9 | |||
| cd7b982385 | |||
| ee59100075 | |||
| f808ac58ef | |||
| f33ada5396 | |||
| 984d781f2f | |||
| dd33e1e8bc | |||
| 2950de29e5 | |||
| 50a8799f9d | |||
| bdb0ce6fc7 | |||
| 18ec2bca96 | |||
| 937ef48794 | |||
| 6331f13e9a | |||
| 5213a2ff8e | |||
| 087d7452fd | |||
| 703094c924 | |||
| eea645c865 | |||
| 324832a189 | |||
| d55d735c51 | |||
| e3ff1b9609 | |||
| 70c31966ca | |||
| 0e4188882f | |||
| 6bf0ce92ba | |||
| d51bdc5086 | |||
| f04f262cee | |||
| 0f172055ef | |||
| e5dd4363f1 | |||
| a3a48bbaac | |||
| 5e716fb9a8 | |||
| 11a36153cc | |||
| 8bee354f04 | |||
| f7dd2b5ce7 | |||
| 8ca10c00bb | |||
| 905c7e8eda | |||
| d4c5e849bf | |||
| 8250a2a021 | |||
| 01b3df7b8c | |||
| daa6e4aff5 | |||
| 23bcb26a58 | |||
| e55f60c30b | |||
| 0d0c16e16d | |||
| 540d5cce7c | |||
| a548b00979 | |||
| 4b7ca6d565 | |||
| 0473fa238c | |||
| cfc7119697 | |||
| 22a6520d3e | |||
| fb25e91191 | |||
| c116b270b6 | |||
| aa2d598689 | |||
| 5ef3bb746b | |||
| 037616e271 | |||
| abbea906f1 | |||
| 9132e216c9 | |||
| 12a7e3c4af | |||
| b40c093917 | |||
| 7ac12455c8 | |||
| 5043eec7a2 | |||
| cf31f53e01 | |||
| cd22d76fa7 | |||
| c8759843f7 | |||
| 781bbcc012 | |||
| 6da29c0686 | |||
| fcdb6fc45a | |||
| e785a99bd7 | |||
| 121e513fdd | |||
| 9cf01ab54f | |||
| c655107681 | |||
| 91a5af6a9a | |||
| 86e74c0a6f | |||
| d8f01f21a0 | |||
| 9fb8626d8c | |||
| 1c52e04cdb | |||
| 798128256e | |||
| 72c2df47fd | |||
| c9c6d4c0c1 | |||
| 2f4f9f3003 | |||
| 22e8f8e5d6 | |||
| 04cf8c3d9a | |||
| 52198be543 | |||
| 80fd5a489b | |||
| b187043ee1 | |||
| 8965752055 | |||
| b796620267 | |||
| 62df81bb4e | |||
| 0a8e0f6178 | |||
| f705020aaa | |||
| 78bd3b9853 | |||
| 992b283597 | |||
| 8fc1ff1d59 | |||
| 3bbde61f39 | |||
| 8d955c4b9d | |||
| 18593c530b | |||
| 78903cd4eb | |||
| eaa143f7d7 | |||
| bcb0e42fa2 | |||
| 8eed435302 | |||
| 0e4a63057f | |||
| 1af5faa440 | |||
| 7412217b0c | |||
| 3e749d75b7 | |||
| 846b5c87c9 | |||
| 7b5e84f80b | |||
| 27822a8a66 | |||
| 36f4e7ec37 | |||
| 187e84ad65 | |||
| 195fe221c4 | |||
| 6f9c19bbf6 | |||
| 65bdab4f7e | |||
| c4d5fcfc22 | |||
| 43232afa62 | |||
| 18908a01d7 | |||
| b22fbfb3bc | |||
| 57aaea88b6 | |||
| 140949c5ea | |||
| 639241e0d3 | |||
| e0eb42bc2d | |||
| 7990b21cc5 | |||
| 2ba5c97709 | |||
| 33b7c85fc2 | |||
| 7f6c02ffdf | |||
| f368616e6f | |||
| dd632f4203 | |||
| a8f3ae501b | |||
| ea76963ac2 | |||
| 99dfa97958 | |||
| f68270a5b3 | |||
| 542ed81034 | |||
| 404a11f5e7 | |||
| 411221070e | |||
| 25d35d0c76 | |||
| c72904d61c | |||
| b94c9acd26 | |||
| bdb4988569 | |||
| 1dafd1352a | |||
| 31fbd3cad7 | |||
| 848bc5ec10 | |||
| 4d0ad826a0 | |||
| 6955820fcc | |||
| ef530624b9 | |||
| 9b9e7dd88f | |||
| a13cc9e961 | |||
| 0d2b923378 | |||
| fba84c8ac8 | |||
| db10fe1b2c | |||
| 175dd980f8 | |||
| 8364020671 | |||
| eba44cd394 | |||
| b3bac8015a | |||
| 0b48afd251 | |||
| 19857930a4 | |||
| d0dbe3ed2f | |||
| 8b7e78b63a | |||
| 92a4ecb523 | |||
| 6a5ad4d728 | |||
| be4aa8daac | |||
| b5eac37782 | |||
| b1ad3c5a39 | |||
| ba16bad029 | |||
| ca8ae4cd72 | |||
| 53d35d74b3 | |||
| 49c139e235 | |||
| caf9c821f3 | |||
| ca4c6db96f | |||
| 6b2298c752 | |||
| a1bf43def9 | |||
| 15e9254e00 | |||
| afe5a72c6f | |||
| 750a8b9ecf | |||
| 27fc3f93e0 | |||
| 8166d4b99b | |||
| b61d2ae2eb | |||
| 4790fe0aea | |||
| bc37b11cee | |||
| 223fed910f | |||
| b85ab7b061 | |||
| 888dc299c9 | |||
| e113dc28ed | |||
| 31e55d2307 | |||
| e90645f248 | |||
| 4bb7ba2571 | |||
| 8d31c25bf8 | |||
| c7ee1c28b2 | |||
| 99b09c8b28 | |||
| a328c4425a | |||
| c0f61ca896 | |||
| 86e70c0961 | |||
| d87a3a039f | |||
| 6279a2c40a | |||
| f377ebea88 | |||
| 7373fef964 | |||
| 41ef86dbda | |||
| 7bc2b2336d | |||
| 0a615e6d78 | |||
| 8f928e16e1 | |||
| 72bc8da1e7 | |||
| 51e349a5db | |||
| 3cbb19ba2c | |||
| ae3a5bf45d | |||
| 9c667f4b78 | |||
| 21195e1bcb | |||
| 03117ac565 | |||
| d13fbdf176 | |||
| 7cecc0e0b6 | |||
| 203e781f5d | |||
| 3eb6cd77cd | |||
| 51855b2405 | |||
| 00cc26d874 | |||
| 02520636ad | |||
| 76813fae8e | |||
| 4693483c2b | |||
| 6dbd12df59 | |||
| 9e84dc5031 | |||
| 8f790360bc | |||
| 808375cea6 | |||
| d9c15db9de | |||
| cf2be8ed43 | |||
| 97339c9b1d | |||
| eb35903de9 | |||
| 43e88e00da | |||
| 1e51f9d77b | |||
| dad738357c | |||
| cb828ab3f2 | |||
| aefaf4a8cc | |||
| b29f37a251 | |||
| 88854020ac | |||
| 14ebe01fc6 | |||
| 771aed0f0f | |||
| 16c57fcd6a | |||
| 75397a7ccb | |||
| b05404f828 | |||
| edea942874 | |||
| 8a8f568b9a | |||
| 4dc8d30c52 | |||
| cb4e08e823 | |||
| c443bf4fa0 | |||
| a6982de822 | |||
| 4f1a663e75 | |||
| dab262d626 | |||
| 87a3f61ca6 | |||
| 506e937a68 | |||
| 5a037c76dd | |||
| 313f622f7e | |||
| 6cba1fe1a2 | |||
| fd2d0e80b7 | |||
| 96ab2e8aca | |||
| 0202988cae | |||
| d619d59947 | |||
| 85b3e48d18 | |||
| 7a9b7d98a1 | |||
| b212acfcaf | |||
| 3a45e5dc70 | |||
| 73190518d5 | |||
| 03f78180dc | |||
| 1c0b8cf842 | |||
| a1624ea2a9 | |||
| 23a050cf1e | |||
| 916f2f96f4 | |||
| 00cc214754 | |||
| b2e38f72b9 | |||
| e7107d238d | |||
| ed9ebdbd1a | |||
| e80676d3d2 | |||
| 02d02fa8f2 | |||
| bd783fb74f | |||
| 50386adf70 | |||
| f4ee6c2890 | |||
| d45aef9fe5 | |||
| a56cd3dddd | |||
| 419e7070ee | |||
| 612cf40b5f | |||
| 8b39882e83 | |||
| e639ae172b | |||
| d0446850ae | |||
| c48465b90b | |||
| f419fd03d2 | |||
| 494812a660 | |||
| eb78f21eec | |||
| 4bda70268b | |||
| f037ce4371 | |||
| 3d2196e35d | |||
| a74f061b02 | |||
| 11ade14676 | |||
| eb562d8784 | |||
| 1ee5b4bfd4 | |||
| d97892080b | |||
| 6f37125724 | |||
| ed1baaade7 | |||
| bb9669f8fd | |||
| bdac715d48 | |||
| acf4971298 | |||
| 249bc83a8c | |||
| 0fbb92f03f | |||
| ca27cb3f82 | |||
| 3a5771a0cc | |||
| 527a2a91ac | |||
| 6763e5c4c6 | |||
| 06918d8310 | |||
| 89ccaa1b57 | |||
| 5d0bdb9bcf | |||
| 31fdcf74a5 | |||
| afca09cc1d | |||
| 531d89d9db | |||
| 6bbbea0bc3 | |||
| e337cd98c8 | |||
| bcbab3b380 | |||
| fb63de7568 | |||
| aa45a4ab13 | |||
| 2af7e2f681 | |||
| 34fd9edce0 | |||
| 2a4cb8c5f9 | |||
| 50ea40bc3a | |||
| a77654052d | |||
| 88aafce552 | |||
| 4e95d6bfff | |||
| 38d0e34fb5 | |||
| 8fbc6b9041 | |||
| e8219d6cf4 | |||
| 6c20fc4ca6 | |||
| 85cd975492 | |||
| 1171a717fe | |||
| bbe5320312 | |||
| 00acb9fddd | |||
| de3d14dc41 | |||
| 67ff9cc9b3 | |||
| af132103a0 | |||
| 3b1124a804 | |||
| f226c2dfd6 | |||
| 69d6e63846 | |||
| 02c3d397ad | |||
| 67a1050646 | |||
| 8cd0775a6c | |||
| 162294d6c6 | |||
| c4dd19dd00 | |||
| d2314422f1 | |||
| 6fedd6f859 | |||
| e52b59665f | |||
| cda9d09689 | |||
| c9237b3f00 | |||
| 18bba66c4a | |||
| 63418c4a8a | |||
| 2e66c6f4e3 | |||
| e9c5df4c13 | |||
| bc7789ad2c | |||
| e3da761249 | |||
| 4082f65afa | |||
| 5d1cab075d | |||
| bcf67f7e59 | |||
| 7d3b1f8e75 | |||
| 119a0588cc | |||
| fab789d9c0 | |||
| ceba81c08f | |||
| a061af0558 | |||
| c7a53846ad | |||
| a683cccf0c | |||
| 50d41e35c1 | |||
| aa0e831cea | |||
| 44e26ccb4f | |||
| 2a783f6e2b | |||
| 6058d6a724 | |||
| 2e9c7eb5fa | |||
| e75465ad10 | |||
| de01ad54e9 | |||
| eeea7ddbe3 | |||
| e0b18bec05 | |||
| 410e89bba9 | |||
| 9ef19dc42b | |||
| 0337d1b82d | |||
| f5bd4c5ccb | |||
| 630f9bc0d4 | |||
| 18b4ffece1 | |||
| f64e1effa3 | |||
| 847fcbb488 | |||
| 88002ec8e7 | |||
| 7f8db6a29d | |||
| b183b82443 | |||
| 5dad27de72 | |||
| 6b66084d0e | |||
| 50b56a7c39 | |||
| 7ab7d14471 | |||
| bdcc179b7b | |||
| 55ffd4b46f | |||
| 7fc5ee70e1 | |||
| a24a335743 | |||
| ef9af71960 | |||
| 925702d315 | |||
| d50dffec8c | |||
| cef2081a13 | |||
| 06c8bde7c9 | |||
| c9bbfa3820 | |||
| eed7cfdc42 | |||
| bd9b0d16ab | |||
| ea6c33b497 | |||
| dc80ade2fb | |||
| f6a06ee8ea | |||
| 2644f27975 | |||
| 3dc68a7b8b | |||
| 97fc1d6239 | |||
| 662f4d22a3 | |||
| b70aa12be9 | |||
| 71f12f5f19 | |||
| e10504eeeb | |||
| 2dea9f3bc0 | |||
| 35c9dda5bc | |||
| e831f3949a | |||
| b0b39cc693 | |||
| fc03be3f73 | |||
| b61f6b81f1 | |||
| f5bc1c1fce | |||
| a729f9568c | |||
| 5749e00377 | |||
| ef73c24a0c | |||
| 94717ee351 | |||
| 9a18ded65b | |||
| b23220f491 | |||
| 6e6bb03246 | |||
| 1ae6bae3b8 | |||
| 1239ca3256 | |||
| 57b7797ea4 | |||
| 5ee5bfd1d5 | |||
| 7229a3e198 | |||
| bee083582f | |||
| 9d7f99f286 | |||
| 6ef403853c | |||
| 6ae7ccb9a1 | |||
| dafc50c463 | |||
| e89e2c931d | |||
| 43a67b99e4 | |||
| 46c48f4f31 | |||
| bf0fe85aa6 | |||
| 42c5bd3a7f | |||
| d170e86c8a | |||
| e3078169b1 | |||
| a33ad123f6 | |||
| 7e14fa2f5c | |||
| ba698b41e1 | |||
| e76215987e | |||
| d1a247af8c | |||
| 2b7e9534f3 | |||
| 38521558a1 | |||
| 100f3d214b | |||
| 1309867611 | |||
| 611f64f2e1 | |||
| f118ebce6e | |||
| 59fc90e556 | |||
| fb9e4d57fb | |||
| 960c38b696 | |||
| 39895e58a6 | |||
| b420d85be5 | |||
| 19ea2f340a | |||
| 11b0d059bf | |||
| c8a25ce544 | |||
| 509583ea2e | |||
| 1c86f3f4bf | |||
| 6d999be590 | |||
| e715772dbf | |||
| 63eb7b7ea8 | |||
| b07683b815 | |||
| 96e97d1691 | |||
| 261d8827e3 | |||
| c3156f2e41 | |||
| 8c08801460 | |||
| aaf1299da7 | |||
| a411355b4f | |||
| eba41066b4 | |||
| f295dff8a2 | |||
| a16815143c | |||
| 6ff3f6ae42 | |||
| 84071881af | |||
| 7cccf7e56e | |||
| 2516d5e381 | |||
| cdec8bac75 | |||
| fa30aae194 | |||
| eb959a3135 | |||
| 24033e0cac | |||
| 71f9a505e0 | |||
| 979b8f017b | |||
| af78f01682 | |||
| 0b16d5c826 | |||
| 597e14bc7e | |||
| 04a95867e2 | |||
| e0d5eb45b7 | |||
| b90cab318e | |||
| 3252b61abe | |||
| 2a2da21ff3 | |||
| 04ef371337 | |||
| 623e0cd369 | |||
| 1f466747f0 | |||
| 2215462f99 | |||
| ac4c0a2e4c | |||
| f7496db5ac | |||
| 3028fe975d | |||
| 23a5034493 | |||
| 65e339f811 | |||
| 2020e48659 | |||
| 9566d6f81f | |||
| a00d734712 | |||
| 27a3e8706a | |||
| e601efcfc0 | |||
| 2bfad9f885 | |||
| e78e984943 | |||
| 242f4b02d0 | |||
| 041be5a1d1 | |||
| 976ce056c1 | |||
| 00c23c73a8 | |||
| 9dd1848337 | |||
| 9b30efd9a2 | |||
| 5853f7cc49 | |||
| 7b00a06f3e | |||
| 9ef9be0f37 | |||
| 13ca419473 | |||
| b80f1a0773 | |||
| e85101c74e | |||
| 90668bdf63 | |||
| e1a00079a5 | |||
| e3add4df42 | |||
| ced1a3bccb | |||
| 8d13a14343 | |||
| 7238e1ea8a | |||
| bd0423f243 | |||
| b0441c134c | |||
| 5860901c30 | |||
| df4eaea4b9 | |||
| e9223d5502 | |||
| 702c5a3e5d | |||
| ccbed95cdc | |||
| c4e2c06cf5 | |||
| a6135738a3 | |||
| ad1ea985b8 | |||
| 2058950d07 | |||
| 4dc27f4489 | |||
| 122daa3220 | |||
| e30928e23d | |||
| 30292306bb | |||
| 2f9aa585f1 | |||
| 9e02c3e5ff | |||
| 7f813a519b | |||
| b5072486b4 | |||
| 50c1cd8215 | |||
| ed4812e6d8 | |||
| 59e0e751f1 | |||
| a5ae5e6c2b | |||
| a2b87e63c9 | |||
| d95c283653 | |||
| 6a9ae0c148 | |||
| 040dd517d8 | |||
| 9663c1ca64 | |||
| ffc2d23be7 | |||
| 49b009e59b | |||
| 778b6a555b | |||
| b3730998e9 | |||
| 837758aebe | |||
| 6ac4149aa1 | |||
| 278584ae6a | |||
| 126f9d5f41 | |||
| ce15ff4e0a | |||
| 382b956beb | |||
| 36deab4909 | |||
| 550fc59d9d | |||
| 9100a57458 | |||
| 74d9bbccd9 | |||
| 75e602a349 | |||
| e73644db10 | |||
| 148e6bdae7 | |||
| 1b540199f0 | |||
| 30b70e9984 | |||
| c1557cff27 | |||
| 2d662f91b3 | |||
| c4a08e52e5 | |||
| 08c371c142 | |||
| 8e62dd460c | |||
| 1f9302dc94 | |||
| 16f445f699 | |||
| b4abd564ec | |||
| 14ffafb0a7 | |||
| e0cc780887 | |||
| ab7d277167 | |||
| 8308c1df4d | |||
| 12c29f5180 | |||
| 2963098870 | |||
| 9f56b0a26d | |||
| 75851312fd | |||
| 3a3be138a5 | |||
| fb7d1f18b0 | |||
| d1c6c7696a | |||
| 5f38d9635d | |||
| 8185ea87b1 | |||
| 98bd07d025 | |||
| c502c70a21 | |||
| d933dc532b | |||
| 42594f0656 | |||
| 4b83ca19c3 | |||
| bab13f5e83 | |||
| 14c5f5a26c | |||
| b26a4f24d4 | |||
| 498ec673dc | |||
| 5c67ab6aec | |||
| f78359b5d7 | |||
| dbce612cb2 | |||
| 4322ffdb0a | |||
| 123b848d74 | |||
| b120fbd2b1 | |||
| d41c3c2de4 | |||
| a3cabd7a9e | |||
| 3dde486126 | |||
| 5978122dda | |||
| 4b4806ce34 | |||
| 987c6cc709 | |||
| 012c7e9bdf | |||
| d06bb265e5 | |||
| e2fb8bf21e | |||
| 60dea2a518 | |||
| 3877f49278 | |||
| a299a5b505 | |||
| a03f7201d2 | |||
| 94e26ee8a0 | |||
| 2d8ad47083 | |||
| e9aec238a1 | |||
| 844c33166c | |||
| cfe9f2159f | |||
| 61bccd5f48 | |||
| e5a608a315 | |||
| 9b4f35eea1 | |||
| 602ae2385b | |||
| 48a4246aac | |||
| a4020cb484 | |||
| d074f29b82 | |||
| a67bef6853 | |||
| 14e666f18d | |||
| cce914091c | |||
| d7cd3db8e2 | |||
| 0d8f844314 | |||
| abd0cb2031 | |||
| 79ef03ed0c | |||
| a3087cb650 | |||
| 9acb9af338 | |||
| 0c9283eb87 | |||
| 493d838201 | |||
| c0523469fa | |||
| 4f00eaa6d4 | |||
| 9706524572 | |||
| 3174b84367 | |||
| 6cccd5ff6c | |||
| 972857268d | |||
| f3cd92cc5e | |||
| 502a49644c | |||
| 51719b5868 | |||
| 17821bd094 | 
							
								
								
									
										24
									
								
								.github/workflows/build.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								.github/workflows/build.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | |||||||
|  |  | ||||||
|  | name: Build | ||||||
|  | on: [push] | ||||||
|  | jobs: | ||||||
|  |   build: | ||||||
|  |     runs-on: ubuntu-latest | ||||||
|  |     steps: | ||||||
|  |       - uses: actions/checkout@v2 | ||||||
|  |       - uses: actions/setup-java@v1 | ||||||
|  |         with: | ||||||
|  |           java-version: 11 | ||||||
|  |       - name: Rewrite version | ||||||
|  |         run: | | ||||||
|  |           branch="`echo "${{ github.ref }}" | grep -o "[^/]*$"`" | ||||||
|  |           cat gradle.properties | sed -e "s/^version=\([0-9\.]*\)/version=\1-branch_$branch-build${{ github.run_number }}/" > gradle.properties.tmp | ||||||
|  |           rm gradle.properties | ||||||
|  |           mv gradle.properties.tmp gradle.properties | ||||||
|  |       - name: Build | ||||||
|  |         run: ./gradlew build | ||||||
|  |       - name: Publish | ||||||
|  |         continue-on-error: true | ||||||
|  |         run: ./gradlew publishAllPublicationsToGiteaRepository | ||||||
|  |         env: | ||||||
|  |           GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} | ||||||
							
								
								
									
										21
									
								
								.github/workflows/dokka_push.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								.github/workflows/dokka_push.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | |||||||
|  | name: Publish KDocs | ||||||
|  | on: | ||||||
|  |   push: | ||||||
|  |     branches: | ||||||
|  |       - master | ||||||
|  | jobs: | ||||||
|  |   publishing: | ||||||
|  |     runs-on: ubuntu-latest | ||||||
|  |     steps: | ||||||
|  |       - uses: actions/checkout@v2 | ||||||
|  |       - uses: actions/setup-java@v1 | ||||||
|  |         with: | ||||||
|  |           java-version: 11 | ||||||
|  |       - name: Build | ||||||
|  |         run: ./gradlew build && ./gradlew dokkaHtml | ||||||
|  |       - name: Publish KDocs | ||||||
|  |         uses: peaceiris/actions-gh-pages@v3 | ||||||
|  |         with: | ||||||
|  |           github_token: ${{ secrets.GITHUB_TOKEN }} | ||||||
|  |           publish_dir: ./dokka/build/dokka/html | ||||||
|  |           publish_branch: kdocs | ||||||
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -11,5 +11,6 @@ out/ | |||||||
|  |  | ||||||
| secret.gradle | secret.gradle | ||||||
| local.properties | local.properties | ||||||
|  | kotlin-js-store | ||||||
|  |  | ||||||
| publishing.sh | publishing.sh | ||||||
|   | |||||||
							
								
								
									
										27
									
								
								.travis.yml
									
									
									
									
									
								
							
							
						
						
									
										27
									
								
								.travis.yml
									
									
									
									
									
								
							| @@ -1,27 +0,0 @@ | |||||||
| language: android |  | ||||||
| install: true |  | ||||||
|  |  | ||||||
| os: linux |  | ||||||
| dist: trusty |  | ||||||
| jdk: oraclejdk8 |  | ||||||
|  |  | ||||||
| android: |  | ||||||
|   components: |  | ||||||
|     - tools |  | ||||||
|     - platform-tools |  | ||||||
|     - build-tools-30.0.2 |  | ||||||
|     - android-30 |  | ||||||
|     - add-on |  | ||||||
|     - extra |  | ||||||
|  |  | ||||||
| before_script: |  | ||||||
|   - yes | /usr/local/android-sdk/tools/bin/sdkmanager "build-tools;30.0.2" |  | ||||||
|   - yes | /usr/local/android-sdk/tools/bin/sdkmanager "platforms;android-30" |  | ||||||
|  |  | ||||||
| jobs: |  | ||||||
|   include: |  | ||||||
|     - stage: build |  | ||||||
|       script: ./gradlew build -s -x jvmTest -x jsIrTest -x jsIrBrowserTest -x jsIrNodeTest -x jsLegacyTest -x jsLegacyBrowserTest -x jsLegacyNodeTest |  | ||||||
| #    Tests are temporarily disabled on public travis due to the problems of launching |  | ||||||
| #    - state: test |  | ||||||
| #      script: ./gradlew allTests |  | ||||||
							
								
								
									
										1448
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										1448
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -22,6 +22,7 @@ You always can look at the <a href="https://github.com/InsanusMokrassar/MicroUti | |||||||
| * `ktor` is a set of modules for `client`s and `server`s | * `ktor` is a set of modules for `client`s and `server`s | ||||||
| * `mime_types` is NOT lightweight set of `MimeType`s with a lot of different objected and serializable (with `Kotlin Serialization`) mime types | * `mime_types` is NOT lightweight set of `MimeType`s with a lot of different objected and serializable (with `Kotlin Serialization`) mime types | ||||||
| * `pagination` is a complex of modules (explanation in [Complex modules structure](#complex-modules-structure) section) for lightweight pagination | * `pagination` is a complex of modules (explanation in [Complex modules structure](#complex-modules-structure) section) for lightweight pagination | ||||||
|  | * `serialization` is a collection of projects with serializers for `kotlinx.serialization` | ||||||
| * `repos` is a complex of modules (explanation in [Complex modules structure](#complex-modules-structure) section) for `KeyValue`/`OneToMany`/`CRUD` repos created to be able to exclude some heavy dependencies when you need some simple and lightweight typical repositories | * `repos` is a complex of modules (explanation in [Complex modules structure](#complex-modules-structure) section) for `KeyValue`/`OneToMany`/`CRUD` repos created to be able to exclude some heavy dependencies when you need some simple and lightweight typical repositories | ||||||
|  |  | ||||||
| ## Complex modules structure | ## Complex modules structure | ||||||
|   | |||||||
| @@ -10,7 +10,7 @@ kotlin { | |||||||
|     sourceSets { |     sourceSets { | ||||||
|         androidMain { |         androidMain { | ||||||
|             dependencies { |             dependencies { | ||||||
|                 api "androidx.appcompat:appcompat-resources:$appcompat_version" |                 api libs.android.appCompat.resources | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -15,7 +15,7 @@ data class AlertAction( | |||||||
|     val callback: (DialogInterface) -> Unit |     val callback: (DialogInterface) -> Unit | ||||||
| ) | ) | ||||||
|  |  | ||||||
| private class ActionViewHolder( | class ActionViewHolder( | ||||||
|     container: ViewGroup, dialogInterfaceGetter: () -> DialogInterface |     container: ViewGroup, dialogInterfaceGetter: () -> DialogInterface | ||||||
| ) : AbstractStandardViewHolder<AlertAction>(container, android.R.layout.simple_list_item_1) { | ) : AbstractStandardViewHolder<AlertAction>(container, android.R.layout.simple_list_item_1) { | ||||||
|     private lateinit var action: AlertAction |     private lateinit var action: AlertAction | ||||||
| @@ -34,10 +34,10 @@ private class ActionViewHolder( | |||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| private class ActionsRecyclerViewAdapter( | class ActionsRecyclerViewAdapter( | ||||||
|     data: List<AlertAction>, |     override val data: List<AlertAction>, | ||||||
|     private val dialogInterfaceGetter: () -> DialogInterface |     private val dialogInterfaceGetter: () -> DialogInterface | ||||||
| ) : RecyclerViewAdapter<AlertAction>(data) { | ) : RecyclerViewAdapter<AlertAction>() { | ||||||
|     override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AbstractViewHolder<AlertAction> = ActionViewHolder( |     override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AbstractViewHolder<AlertAction> = ActionViewHolder( | ||||||
|         parent, dialogInterfaceGetter |         parent, dialogInterfaceGetter | ||||||
|     ) |     ) | ||||||
|   | |||||||
| @@ -10,12 +10,13 @@ kotlin { | |||||||
|     sourceSets { |     sourceSets { | ||||||
|         commonMain { |         commonMain { | ||||||
|             dependencies { |             dependencies { | ||||||
|                 api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version" |                 api libs.kt.coroutines | ||||||
|  |                 api project(":micro_utils.common") | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         androidMain { |         androidMain { | ||||||
|             dependencies { |             dependencies { | ||||||
|                 api "androidx.recyclerview:recyclerview:$androidx_recycler_version" |                 api libs.android.recyclerView | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -1,12 +1,21 @@ | |||||||
| package dev.inmo.micro_utils.android.recyclerview | package dev.inmo.micro_utils.android.recyclerview | ||||||
|  |  | ||||||
| import android.view.View | import android.view.View | ||||||
|  | import android.view.ViewGroup | ||||||
| import androidx.recyclerview.widget.RecyclerView | import androidx.recyclerview.widget.RecyclerView | ||||||
|  | import kotlinx.coroutines.flow.* | ||||||
|  |  | ||||||
|  |  | ||||||
| abstract class RecyclerViewAdapter<T>( | abstract class RecyclerViewAdapter<T>: RecyclerView.Adapter<AbstractViewHolder<T>>() { | ||||||
|     val data: List<T> |     protected abstract val data: List<T> | ||||||
| ): RecyclerView.Adapter<AbstractViewHolder<T>>() { |  | ||||||
|  |     private val _dataCountState by lazy { | ||||||
|  |         MutableStateFlow<Int>(data.size) | ||||||
|  |     } | ||||||
|  |     val dataCountState: StateFlow<Int> by lazy { | ||||||
|  |         _dataCountState.asStateFlow() | ||||||
|  |     } | ||||||
|  |  | ||||||
|     var emptyView: View? = null |     var emptyView: View? = null | ||||||
|         set(value) { |         set(value) { | ||||||
|             field = value |             field = value | ||||||
| @@ -18,31 +27,37 @@ abstract class RecyclerViewAdapter<T>( | |||||||
|             object : RecyclerView.AdapterDataObserver() { |             object : RecyclerView.AdapterDataObserver() { | ||||||
|                 override fun onItemRangeChanged(positionStart: Int, itemCount: Int) { |                 override fun onItemRangeChanged(positionStart: Int, itemCount: Int) { | ||||||
|                     super.onItemRangeChanged(positionStart, itemCount) |                     super.onItemRangeChanged(positionStart, itemCount) | ||||||
|  |                     _dataCountState.value = data.size | ||||||
|                     checkEmpty() |                     checkEmpty() | ||||||
|                 } |                 } | ||||||
|  |  | ||||||
|                 override fun onItemRangeChanged(positionStart: Int, itemCount: Int, payload: Any?) { |                 override fun onItemRangeChanged(positionStart: Int, itemCount: Int, payload: Any?) { | ||||||
|                     super.onItemRangeChanged(positionStart, itemCount, payload) |                     super.onItemRangeChanged(positionStart, itemCount, payload) | ||||||
|  |                     _dataCountState.value = data.size | ||||||
|                     checkEmpty() |                     checkEmpty() | ||||||
|                 } |                 } | ||||||
|  |  | ||||||
|                 override fun onChanged() { |                 override fun onChanged() { | ||||||
|                     super.onChanged() |                     super.onChanged() | ||||||
|  |                     _dataCountState.value = data.size | ||||||
|                     checkEmpty() |                     checkEmpty() | ||||||
|                 } |                 } | ||||||
|  |  | ||||||
|                 override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) { |                 override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) { | ||||||
|                     super.onItemRangeRemoved(positionStart, itemCount) |                     super.onItemRangeRemoved(positionStart, itemCount) | ||||||
|  |                     _dataCountState.value = data.size | ||||||
|                     checkEmpty() |                     checkEmpty() | ||||||
|                 } |                 } | ||||||
|  |  | ||||||
|                 override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) { |                 override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) { | ||||||
|                     super.onItemRangeMoved(fromPosition, toPosition, itemCount) |                     super.onItemRangeMoved(fromPosition, toPosition, itemCount) | ||||||
|  |                     _dataCountState.value = data.size | ||||||
|                     checkEmpty() |                     checkEmpty() | ||||||
|                 } |                 } | ||||||
|  |  | ||||||
|                 override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { |                 override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { | ||||||
|                     super.onItemRangeInserted(positionStart, itemCount) |                     super.onItemRangeInserted(positionStart, itemCount) | ||||||
|  |                     _dataCountState.value = data.size | ||||||
|                     checkEmpty() |                     checkEmpty() | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
| @@ -58,7 +73,7 @@ abstract class RecyclerViewAdapter<T>( | |||||||
|  |  | ||||||
|     private fun checkEmpty() { |     private fun checkEmpty() { | ||||||
|         emptyView ?. let { |         emptyView ?. let { | ||||||
|             if (data.isEmpty()) { |             if (dataCountState.value == 0) { | ||||||
|                 it.visibility = View.VISIBLE |                 it.visibility = View.VISIBLE | ||||||
|             } else { |             } else { | ||||||
|                 it.visibility = View.GONE |                 it.visibility = View.GONE | ||||||
| @@ -66,3 +81,11 @@ abstract class RecyclerViewAdapter<T>( | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | fun <T> RecyclerViewAdapter( | ||||||
|  |     data: List<T>, | ||||||
|  |     onCreateViewHolder: (parent: ViewGroup, viewType: Int) -> AbstractViewHolder<T> | ||||||
|  | ) = object : RecyclerViewAdapter<T>() { | ||||||
|  |     override val data: List<T> = data | ||||||
|  |     override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AbstractViewHolder<T> = onCreateViewHolder(parent, viewType) | ||||||
|  | } | ||||||
|   | |||||||
| @@ -0,0 +1,50 @@ | |||||||
|  | package dev.inmo.micro_utils.android.recyclerview | ||||||
|  |  | ||||||
|  | import dev.inmo.micro_utils.common.Diff | ||||||
|  | import dev.inmo.micro_utils.common.PreviewFeature | ||||||
|  | import kotlinx.coroutines.* | ||||||
|  | import kotlinx.coroutines.flow.* | ||||||
|  |  | ||||||
|  | @PreviewFeature("This feature in preview state and may contains different bugs. " + | ||||||
|  |                 "Besides, this feature can be changed in future in non-compatible way") | ||||||
|  | abstract class StateFlowBasedRecyclerViewAdapter<T>( | ||||||
|  |     listeningScope: CoroutineScope, | ||||||
|  |     dataState: StateFlow<List<T>> | ||||||
|  | ) : RecyclerViewAdapter<T>() { | ||||||
|  |     override var data: List<T> = emptyList() | ||||||
|  |  | ||||||
|  |     init { | ||||||
|  |         dataState.onEach { | ||||||
|  |             try { | ||||||
|  |                 val diffForRemoves = Diff(data, it) | ||||||
|  |                 val removedIndexes = diffForRemoves.removed.map { it.index } | ||||||
|  |                 val leftRemove = removedIndexes.toMutableList() | ||||||
|  |                 data = data.filterIndexed { i, _ -> | ||||||
|  |                     if (i in leftRemove) { | ||||||
|  |                         leftRemove.remove(i) | ||||||
|  |                         true | ||||||
|  |                     } else { | ||||||
|  |                         false | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |                 withContext(Dispatchers.Main) { | ||||||
|  |                     removedIndexes.sortedDescending().forEach { | ||||||
|  |                         notifyItemRemoved(it) | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |                 val diffAddsAndReplaces = Diff(data, it) | ||||||
|  |                 data = it | ||||||
|  |                 withContext(Dispatchers.Main) { | ||||||
|  |                     diffAddsAndReplaces.replaced.forEach { (from, to) -> | ||||||
|  |                         notifyItemMoved(from.index, to.index) | ||||||
|  |                     } | ||||||
|  |                     diffAddsAndReplaces.added.forEach { | ||||||
|  |                         notifyItemInserted(it.index) | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } catch (e: Throwable) { | ||||||
|  |                 // currently do nothing | ||||||
|  |             } | ||||||
|  |         }.launchIn(listeningScope) | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										27
									
								
								build.gradle
									
									
									
									
									
								
							
							
						
						
									
										27
									
								
								build.gradle
									
									
									
									
									
								
							| @@ -1,6 +1,5 @@ | |||||||
| buildscript { | buildscript { | ||||||
|     repositories { |     repositories { | ||||||
|         jcenter() |  | ||||||
|         google() |         google() | ||||||
|         mavenCentral() |         mavenCentral() | ||||||
|         mavenLocal() |         mavenLocal() | ||||||
| @@ -8,22 +7,32 @@ buildscript { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     dependencies { |     dependencies { | ||||||
|         classpath 'com.android.tools.build:gradle:4.0.2' |         classpath libs.buildscript.kt.gradle | ||||||
|         classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" |         classpath libs.buildscript.kt.serialization | ||||||
|         classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" |         classpath libs.buildscript.kt.ksp | ||||||
|         classpath "com.getkeepsafe.dexcount:dexcount-gradle-plugin:$dexcount_version" |         classpath libs.buildscript.jb.dokka | ||||||
|         classpath "com.github.breadmoirai:github-release:$github_release_plugin_version" |         classpath libs.buildscript.gh.release | ||||||
|         classpath "org.jetbrains.dokka:dokka-gradle-plugin:$dokka_version" |         classpath libs.buildscript.android.gradle | ||||||
|  |         classpath libs.buildscript.android.dexcount | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| allprojects { | allprojects { | ||||||
|     repositories { |     repositories { | ||||||
|         mavenLocal() |         mavenLocal() | ||||||
|         jcenter() |  | ||||||
|         mavenCentral() |         mavenCentral() | ||||||
|         google() |         google() | ||||||
|         maven { url "https://kotlin.bintray.com/kotlinx" } |         maven { url "https://maven.pkg.jetbrains.space/public/p/compose/dev" } | ||||||
|  |         maven { url "https://git.inmo.dev/api/packages/InsanusMokrassar/maven" } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // temporal crutch until legacy tests will be stabled or legacy target will be removed | ||||||
|  |     if (it != rootProject.findProject("docs")) { | ||||||
|  |         tasks.whenTaskAdded { task -> | ||||||
|  |             if(task.name == "jsLegacyBrowserTest" || task.name == "jsLegacyNodeTest") { | ||||||
|  |                 task.enabled = false | ||||||
|  |             } | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -5,3 +5,31 @@ plugins { | |||||||
| } | } | ||||||
|  |  | ||||||
| apply from: "$mppProjectWithSerializationPresetPath" | apply from: "$mppProjectWithSerializationPresetPath" | ||||||
|  |  | ||||||
|  | kotlin { | ||||||
|  |     sourceSets { | ||||||
|  |         jvmMain { | ||||||
|  |             dependencies { | ||||||
|  |                 api project(":micro_utils.coroutines") | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         androidMain { | ||||||
|  |             dependencies { | ||||||
|  |                 api project(":micro_utils.coroutines") | ||||||
|  |                 api libs.android.fragment | ||||||
|  |             } | ||||||
|  |             dependsOn jvmMain | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         linuxX64Main { | ||||||
|  |             dependencies { | ||||||
|  |                 api libs.okio | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         mingwX64Main { | ||||||
|  |             dependencies { | ||||||
|  |                 api libs.okio | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|   | |||||||
							
								
								
									
										18
									
								
								common/compose/build.gradle
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								common/compose/build.gradle
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | |||||||
|  | plugins { | ||||||
|  |     id "org.jetbrains.kotlin.multiplatform" | ||||||
|  |     id "org.jetbrains.kotlin.plugin.serialization" | ||||||
|  |     id "com.android.library" | ||||||
|  |     alias(libs.plugins.jb.compose) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | apply from: "$mppProjectWithSerializationAndComposePresetPath" | ||||||
|  |  | ||||||
|  | kotlin { | ||||||
|  |     sourceSets { | ||||||
|  |         commonMain { | ||||||
|  |             dependencies { | ||||||
|  |                 api project(":micro_utils.common") | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,16 @@ | |||||||
|  | package dev.inmo.micro_utils.common.compose | ||||||
|  |  | ||||||
|  | import androidx.compose.runtime.DisposableEffectResult | ||||||
|  |  | ||||||
|  | class DefaultDisposableEffectResult( | ||||||
|  |     private val onDispose: () -> Unit | ||||||
|  | ) : DisposableEffectResult { | ||||||
|  |     override fun dispose() { | ||||||
|  |         onDispose() | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     companion object { | ||||||
|  |         val DoNothing = DefaultDisposableEffectResult {} | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
| @@ -0,0 +1,10 @@ | |||||||
|  | package dev.inmo.micro_utils.common.compose | ||||||
|  |  | ||||||
|  | import androidx.compose.runtime.MutableState | ||||||
|  | import androidx.compose.runtime.State | ||||||
|  | import androidx.compose.runtime.derivedStateOf | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Converts current [MutableState] to immutable [State] using [derivedStateOf] | ||||||
|  |  */ | ||||||
|  | fun <T> MutableState<T>.asState(): State<T> = derivedStateOf { this.value } | ||||||
| @@ -0,0 +1,9 @@ | |||||||
|  | package dev.inmo.micro_utils.common.compose | ||||||
|  |  | ||||||
|  | import androidx.compose.runtime.Composition | ||||||
|  | import dev.inmo.micro_utils.common.onRemoved | ||||||
|  | import org.w3c.dom.Element | ||||||
|  |  | ||||||
|  | fun Composition.linkWithElement(element: Element) { | ||||||
|  |     element.onRemoved { dispose() } | ||||||
|  | } | ||||||
| @@ -0,0 +1,10 @@ | |||||||
|  | package dev.inmo.micro_utils.common.compose | ||||||
|  |  | ||||||
|  | import org.jetbrains.compose.web.attributes.ATarget | ||||||
|  |  | ||||||
|  | fun openLink(link: String, mode: ATarget = ATarget.Blank, features: String = "") = dev.inmo.micro_utils.common.openLink( | ||||||
|  |     link, | ||||||
|  |     mode.targetStr, | ||||||
|  |     features | ||||||
|  | ) | ||||||
|  |  | ||||||
| @@ -0,0 +1,13 @@ | |||||||
|  | package dev.inmo.micro_utils.common.compose | ||||||
|  |  | ||||||
|  | import androidx.compose.runtime.* | ||||||
|  | import org.jetbrains.compose.web.dom.DOMScope | ||||||
|  | import org.w3c.dom.Element | ||||||
|  |  | ||||||
|  | fun <TElement : Element> renderComposableAndLinkToRoot( | ||||||
|  |     root: TElement, | ||||||
|  |     monotonicFrameClock: MonotonicFrameClock = DefaultMonotonicFrameClock, | ||||||
|  |     content: @Composable DOMScope<TElement>.() -> Unit | ||||||
|  | ): Composition = org.jetbrains.compose.web.renderComposable(root, monotonicFrameClock, content).apply { | ||||||
|  |     linkWithElement(root) | ||||||
|  | } | ||||||
| @@ -0,0 +1,43 @@ | |||||||
|  | package dev.inmo.micro_utils.common.compose | ||||||
|  |  | ||||||
|  | import org.jetbrains.compose.web.css.* | ||||||
|  |  | ||||||
|  | object SkeletonAnimation : StyleSheet() { | ||||||
|  |     val skeletonKeyFrames: CSSNamedKeyframes by keyframes { | ||||||
|  |         to { backgroundPosition("-20% 0") } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fun CSSBuilder.includeSkeletonStyle( | ||||||
|  |         duration: CSSSizeValue<out CSSUnitTime> = 2.s, | ||||||
|  |         timingFunction: AnimationTimingFunction = AnimationTimingFunction.EaseInOut, | ||||||
|  |         iterationCount: Int? = null, | ||||||
|  |         direction: AnimationDirection = AnimationDirection.Normal, | ||||||
|  |         keyFrames: CSSNamedKeyframes = skeletonKeyFrames, | ||||||
|  |         hideChildren: Boolean = true, | ||||||
|  |         hideText: Boolean = hideChildren | ||||||
|  |     ) { | ||||||
|  |         backgroundImage("linear-gradient(110deg, rgb(236, 236, 236) 40%, rgb(245, 245, 245) 50%, rgb(236, 236, 236) 65%)") | ||||||
|  |         backgroundSize("200% 100%") | ||||||
|  |         backgroundPosition("180% 0") | ||||||
|  |         animation(keyFrames) { | ||||||
|  |             duration(duration) | ||||||
|  |             timingFunction(timingFunction) | ||||||
|  |             iterationCount(iterationCount) | ||||||
|  |             direction(direction) | ||||||
|  |         } | ||||||
|  |         if (hideText) { | ||||||
|  |             property("color", "${Color.transparent} !important") | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (hideChildren) { | ||||||
|  |             child(self, universal) style { | ||||||
|  |                 property("visibility", "hidden") | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     val skeleton by style { | ||||||
|  |         includeSkeletonStyle() | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
							
								
								
									
										1
									
								
								common/compose/src/main/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								common/compose/src/main/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | <manifest package="dev.inmo.micro_utils.common.compose"/> | ||||||
| @@ -1,3 +1,5 @@ | |||||||
|  | @file:Suppress("OPT_IN_IS_NOT_ENABLED") | ||||||
|  |  | ||||||
| package dev.inmo.micro_utils.common | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
| @RequiresOptIn( | @RequiresOptIn( | ||||||
| @@ -12,11 +14,9 @@ package dev.inmo.micro_utils.common | |||||||
|     AnnotationTarget.PROPERTY_GETTER, |     AnnotationTarget.PROPERTY_GETTER, | ||||||
|     AnnotationTarget.PROPERTY_SETTER, |     AnnotationTarget.PROPERTY_SETTER, | ||||||
|     AnnotationTarget.FUNCTION, |     AnnotationTarget.FUNCTION, | ||||||
|     AnnotationTarget.TYPE, |     AnnotationTarget.TYPEALIAS | ||||||
|     AnnotationTarget.TYPEALIAS, |  | ||||||
|     AnnotationTarget.TYPE_PARAMETER |  | ||||||
| ) | ) | ||||||
| annotation class PreviewFeature | annotation class PreviewFeature(val message: String = "It is possible, that behaviour of this thing will be changed or removed in future releases") | ||||||
|  |  | ||||||
| @RequiresOptIn( | @RequiresOptIn( | ||||||
|     "This thing is marked as warned. See message of warn to get more info", |     "This thing is marked as warned. See message of warn to get more info", | ||||||
| @@ -30,8 +30,6 @@ annotation class PreviewFeature | |||||||
|     AnnotationTarget.PROPERTY_GETTER, |     AnnotationTarget.PROPERTY_GETTER, | ||||||
|     AnnotationTarget.PROPERTY_SETTER, |     AnnotationTarget.PROPERTY_SETTER, | ||||||
|     AnnotationTarget.FUNCTION, |     AnnotationTarget.FUNCTION, | ||||||
|     AnnotationTarget.TYPE, |     AnnotationTarget.TYPEALIAS | ||||||
|     AnnotationTarget.TYPEALIAS, |  | ||||||
|     AnnotationTarget.TYPE_PARAMETER |  | ||||||
| ) | ) | ||||||
| annotation class Warning(val message: String) | annotation class Warning(val message: String) | ||||||
|   | |||||||
| @@ -1,10 +0,0 @@ | |||||||
| package dev.inmo.micro_utils.common |  | ||||||
|  |  | ||||||
| @Suppress("NOTHING_TO_INLINE") |  | ||||||
| inline fun <T : Comparable<T>> T.clamp(min: T, max: T): T { |  | ||||||
|     return when { |  | ||||||
|         this < min -> min |  | ||||||
|         this > max -> max |  | ||||||
|         else -> this |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -2,6 +2,8 @@ | |||||||
|  |  | ||||||
| package dev.inmo.micro_utils.common | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | import kotlinx.serialization.Serializable | ||||||
|  |  | ||||||
| private inline fun <T> getObject( | private inline fun <T> getObject( | ||||||
|     additional: MutableList<T>, |     additional: MutableList<T>, | ||||||
|     iterator: Iterator<T> |     iterator: Iterator<T> | ||||||
| @@ -14,35 +16,49 @@ private inline fun <T> getObject( | |||||||
| /** | /** | ||||||
|  * Diff object which contains information about differences between two [Iterable]s |  * Diff object which contains information about differences between two [Iterable]s | ||||||
|  * |  * | ||||||
|  |  * See tests for more info | ||||||
|  |  * | ||||||
|  |  * @param removed The objects which has been presented in the old collection but absent in new one. Index here is the index in the old collection | ||||||
|  |  * @param added The object which appear in new collection only. Indexes here show the index in the new collection | ||||||
|  |  * @param replaced Pair of old-new changes. First object has been presented in the old collection on its | ||||||
|  |  * [IndexedValue.index] place, the second one is the object in new collection. Both have indexes due to the fact that in | ||||||
|  |  * case when some value has been replaced after adds or removes in original collection the object index will be changed | ||||||
|  |  * | ||||||
|  * @see calculateDiff |  * @see calculateDiff | ||||||
|  */ |  */ | ||||||
|  | @Serializable | ||||||
| data class Diff<T> internal constructor( | data class Diff<T> internal constructor( | ||||||
|     val removed: List<IndexedValue<T>>, |     val removed: List<@Serializable(IndexedValueSerializer::class) IndexedValue<T>>, | ||||||
|     /** |     /** | ||||||
|      * Old-New values pairs |      * Old-New values pairs | ||||||
|      */ |      */ | ||||||
|     val replaced: List<Pair<IndexedValue<T>, IndexedValue<T>>>, |     val replaced: List<Pair<@Serializable(IndexedValueSerializer::class) IndexedValue<T>, @Serializable(IndexedValueSerializer::class) IndexedValue<T>>>, | ||||||
|     val added: List<IndexedValue<T>> |     val added: List<@Serializable(IndexedValueSerializer::class) IndexedValue<T>> | ||||||
| ) | ) { | ||||||
|  |     fun isEmpty(): Boolean = removed.isEmpty() && replaced.isEmpty() && added.isEmpty() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fun <T> emptyDiff(): Diff<T> = Diff(emptyList(), emptyList(), emptyList()) | ||||||
|  |  | ||||||
| private inline fun <T> performChanges( | private inline fun <T> performChanges( | ||||||
|     potentialChanges: MutableList<Pair<IndexedValue<T>?, IndexedValue<T>?>>, |     potentialChanges: MutableList<Pair<IndexedValue<T>?, IndexedValue<T>?>>, | ||||||
|     additionalsInOld: MutableList<T>, |     additionsInOld: MutableList<T>, | ||||||
|     additionalsInNew: MutableList<T>, |     additionsInNew: MutableList<T>, | ||||||
|     changedList: MutableList<Pair<IndexedValue<T>, IndexedValue<T>>>, |     changedList: MutableList<Pair<IndexedValue<T>, IndexedValue<T>>>, | ||||||
|     removedList: MutableList<IndexedValue<T>>, |     removedList: MutableList<IndexedValue<T>>, | ||||||
|     addedList: MutableList<IndexedValue<T>>, |     addedList: MutableList<IndexedValue<T>>, | ||||||
|     strictComparison: Boolean |     comparisonFun: (T?, T?) -> Boolean | ||||||
| ) { | ) { | ||||||
|     var i = -1 |     var i = -1 | ||||||
|     val (oldObject, newObject) = potentialChanges.lastOrNull() ?: return |     val (oldObject, newObject) = potentialChanges.lastOrNull() ?: return | ||||||
|     for ((old, new) in potentialChanges.take(potentialChanges.size - 1)) { |     for ((old, new) in potentialChanges.take(potentialChanges.size - 1)) { | ||||||
|         i++ |         i++ | ||||||
|         val oldOneEqualToNewObject = old ?.value === newObject ?.value || (old ?.value == newObject ?.value && !strictComparison) |         val oldOneEqualToNewObject = comparisonFun(old ?.value, newObject ?.value) | ||||||
|         val newOneEqualToOldObject = new ?.value === oldObject ?.value || (new ?.value == oldObject ?.value && !strictComparison) |         val newOneEqualToOldObject = comparisonFun(new ?.value, oldObject ?.value) | ||||||
|         if (oldOneEqualToNewObject || newOneEqualToOldObject) { |         if (oldOneEqualToNewObject || newOneEqualToOldObject) { | ||||||
|             changedList.addAll( |             changedList.addAll( | ||||||
|                 potentialChanges.take(i).mapNotNull { |                 potentialChanges.take(i).mapNotNull { | ||||||
|  |                     @Suppress("UNCHECKED_CAST") | ||||||
|                     if (it.first != null && it.second != null) it as Pair<IndexedValue<T>, IndexedValue<T>> else null |                     if (it.first != null && it.second != null) it as Pair<IndexedValue<T>, IndexedValue<T>> else null | ||||||
|                 } |                 } | ||||||
|             ) |             ) | ||||||
| @@ -52,20 +68,20 @@ private inline fun <T> performChanges( | |||||||
|                     newPotentials.first().second ?.let { addedList.add(it) } |                     newPotentials.first().second ?.let { addedList.add(it) } | ||||||
|                     newPotentials.drop(1).take(newPotentials.size - 2).forEach { (oldOne, newOne) -> |                     newPotentials.drop(1).take(newPotentials.size - 2).forEach { (oldOne, newOne) -> | ||||||
|                         addedList.add(newOne!!) |                         addedList.add(newOne!!) | ||||||
|                         oldOne ?.let { additionalsInOld.add(oldOne.value) } |                         oldOne ?.let { additionsInOld.add(oldOne.value) } | ||||||
|                     } |                     } | ||||||
|                     if (newPotentials.size > 1) { |                     if (newPotentials.size > 1) { | ||||||
|                         newPotentials.last().first ?.value ?.let { additionalsInOld.add(it) } |                         newPotentials.last().first ?.value ?.let { additionsInOld.add(it) } | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|                 newOneEqualToOldObject -> { |                 newOneEqualToOldObject -> { | ||||||
|                     newPotentials.first().first ?.let { removedList.add(it) } |                     newPotentials.first().first ?.let { removedList.add(it) } | ||||||
|                     newPotentials.drop(1).take(newPotentials.size - 2).forEach { (oldOne, newOne) -> |                     newPotentials.drop(1).take(newPotentials.size - 2).forEach { (oldOne, newOne) -> | ||||||
|                         removedList.add(oldOne!!) |                         removedList.add(oldOne!!) | ||||||
|                         newOne ?.let { additionalsInNew.add(newOne.value) } |                         newOne ?.let { additionsInNew.add(newOne.value) } | ||||||
|                     } |                     } | ||||||
|                     if (newPotentials.size > 1) { |                     if (newPotentials.size > 1) { | ||||||
|                         newPotentials.last().second ?.value ?.let { additionalsInNew.add(it) } |                         newPotentials.last().second ?.value ?.let { additionsInNew.add(it) } | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
| @@ -92,7 +108,7 @@ private inline fun <T> performChanges( | |||||||
|  */ |  */ | ||||||
| fun <T> Iterable<T>.calculateDiff( | fun <T> Iterable<T>.calculateDiff( | ||||||
|     other: Iterable<T>, |     other: Iterable<T>, | ||||||
|     strictComparison: Boolean = false |     comparisonFun: (T?, T?) -> Boolean | ||||||
| ): Diff<T> { | ): Diff<T> { | ||||||
|     var i = -1 |     var i = -1 | ||||||
|     var j = -1 |     var j = -1 | ||||||
| @@ -120,27 +136,60 @@ fun <T> Iterable<T>.calculateDiff( | |||||||
|         } |         } | ||||||
|  |  | ||||||
|         when { |         when { | ||||||
|             oldObject === newObject || (oldObject == newObject && !strictComparison) -> { |             comparisonFun(oldObject, newObject) -> { | ||||||
|                 changedObjects.addAll(potentiallyChangedObjects.map { it as Pair<IndexedValue<T>, IndexedValue<T>> }) |                 changedObjects.addAll(potentiallyChangedObjects.map { | ||||||
|  |                     @Suppress("UNCHECKED_CAST") | ||||||
|  |                     it as Pair<IndexedValue<T>, IndexedValue<T>> | ||||||
|  |                 }) | ||||||
|                 potentiallyChangedObjects.clear() |                 potentiallyChangedObjects.clear() | ||||||
|             } |             } | ||||||
|             else -> { |             else -> { | ||||||
|                 potentiallyChangedObjects.add(oldObject ?.let { IndexedValue(i, oldObject) } to newObject ?.let { IndexedValue(j, newObject) }) |                 potentiallyChangedObjects.add(oldObject ?.let { IndexedValue(i, oldObject) } to newObject ?.let { IndexedValue(j, newObject) }) | ||||||
|                 val previousOldsAdditionsSize = additionalInOld.size |                 val previousOldsAdditionsSize = additionalInOld.size | ||||||
|                 val previousNewsAdditionsSize = additionalInNew.size |                 val previousNewsAdditionsSize = additionalInNew.size | ||||||
|                 performChanges(potentiallyChangedObjects, additionalInOld, additionalInNew, changedObjects, removedObjects, addedObjects, strictComparison) |                 performChanges(potentiallyChangedObjects, additionalInOld, additionalInNew, changedObjects, removedObjects, addedObjects, comparisonFun) | ||||||
|                 i -= (additionalInOld.size - previousOldsAdditionsSize) |                 i -= (additionalInOld.size - previousOldsAdditionsSize) | ||||||
|                 j -= (additionalInNew.size - previousNewsAdditionsSize) |                 j -= (additionalInNew.size - previousNewsAdditionsSize) | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|     potentiallyChangedObjects.add(null to null) |     potentiallyChangedObjects.add(null to null) | ||||||
|     performChanges(potentiallyChangedObjects, additionalInOld, additionalInNew, changedObjects, removedObjects, addedObjects, strictComparison) |     performChanges(potentiallyChangedObjects, additionalInOld, additionalInNew, changedObjects, removedObjects, addedObjects, comparisonFun) | ||||||
|  |  | ||||||
|     return Diff(removedObjects.toList(), changedObjects.toList(), addedObjects.toList()) |     return Diff(removedObjects.toList(), changedObjects.toList(), addedObjects.toList()) | ||||||
| } | } | ||||||
|  |  | ||||||
| inline fun <T> Diff(old: Iterable<T>, new: Iterable<T>) = old.calculateDiff(new) | /** | ||||||
|  |  * Calculating [Diff] object | ||||||
|  |  * | ||||||
|  |  * @param strictComparison If this parameter set to true, objects which are not equal by links will be used as different | ||||||
|  |  * objects. For example, in case of two "Example" string they will be equal by value, but CAN be different by links | ||||||
|  |  */ | ||||||
|  | fun <T> Iterable<T>.calculateDiff( | ||||||
|  |     other: Iterable<T>, | ||||||
|  |     strictComparison: Boolean = false | ||||||
|  | ): Diff<T> = calculateDiff( | ||||||
|  |     other, | ||||||
|  |     comparisonFun = if (strictComparison) { | ||||||
|  |         { t1, t2 -> | ||||||
|  |             t1 === t2 | ||||||
|  |         } | ||||||
|  |     } else { | ||||||
|  |         { t1, t2 -> | ||||||
|  |             t1 === t2 || t1 == t2 // small optimization for cases when t1 and t2 are the same - comparison will be faster potentially | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | ) | ||||||
|  | inline fun <T> Iterable<T>.diff( | ||||||
|  |     other: Iterable<T>, | ||||||
|  |     strictComparison: Boolean = false | ||||||
|  | ): Diff<T> = calculateDiff(other, strictComparison) | ||||||
|  | inline fun <T> Iterable<T>.diff( | ||||||
|  |     other: Iterable<T>, | ||||||
|  |     noinline comparisonFun: (T?, T?) -> Boolean | ||||||
|  | ): Diff<T> = calculateDiff(other, comparisonFun) | ||||||
|  |  | ||||||
|  | inline fun <T> Diff(old: Iterable<T>, new: Iterable<T>) = old.calculateDiff(new, strictComparison = false) | ||||||
| inline fun <T> StrictDiff(old: Iterable<T>, new: Iterable<T>) = old.calculateDiff(new, true) | inline fun <T> StrictDiff(old: Iterable<T>, new: Iterable<T>) = old.calculateDiff(new, true) | ||||||
|  |  | ||||||
| /** | /** | ||||||
| @@ -149,3 +198,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) | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -7,9 +7,17 @@ import kotlinx.serialization.encoding.Decoder | |||||||
| import kotlinx.serialization.encoding.Encoder | import kotlinx.serialization.encoding.Encoder | ||||||
|  |  | ||||||
| typealias ByteArrayAllocator = () -> ByteArray | typealias ByteArrayAllocator = () -> ByteArray | ||||||
|  | typealias SuspendByteArrayAllocator = suspend () -> ByteArray | ||||||
|  |  | ||||||
| val ByteArray.asAllocator: ByteArrayAllocator | val ByteArray.asAllocator: ByteArrayAllocator | ||||||
|     get() = { this } |     get() = { this } | ||||||
|  | val ByteArray.asSuspendAllocator: SuspendByteArrayAllocator | ||||||
|  |     get() = { this } | ||||||
|  | val ByteArrayAllocator.asSuspendAllocator: SuspendByteArrayAllocator | ||||||
|  |     get() = { this() } | ||||||
|  | suspend fun SuspendByteArrayAllocator.asAllocator(): ByteArrayAllocator { | ||||||
|  |     return invoke().asAllocator | ||||||
|  | } | ||||||
|  |  | ||||||
| object ByteArrayAllocatorSerializer : KSerializer<ByteArrayAllocator> { | object ByteArrayAllocatorSerializer : KSerializer<ByteArrayAllocator> { | ||||||
|     private val realSerializer = ByteArraySerializer() |     private val realSerializer = ByteArraySerializer() | ||||||
| @@ -17,7 +25,7 @@ object ByteArrayAllocatorSerializer : KSerializer<ByteArrayAllocator> { | |||||||
|  |  | ||||||
|     override fun deserialize(decoder: Decoder): ByteArrayAllocator { |     override fun deserialize(decoder: Decoder): ByteArrayAllocator { | ||||||
|         val bytes = realSerializer.deserialize(decoder) |         val bytes = realSerializer.deserialize(decoder) | ||||||
|         return { bytes } |         return bytes.asAllocator | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     override fun serialize(encoder: Encoder, value: ByteArrayAllocator) { |     override fun serialize(encoder: Encoder, value: ByteArrayAllocator) { | ||||||
|   | |||||||
| @@ -0,0 +1,3 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | fun <T> Iterable<T?>.firstNotNull() = first { it != null }!! | ||||||
| @@ -0,0 +1,59 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | inline fun <I, R> Iterable<I>.joinTo( | ||||||
|  |     separatorFun: (I) -> R?, | ||||||
|  |     prefix: R? = null, | ||||||
|  |     postfix: R? = null, | ||||||
|  |     transform: (I) -> R? | ||||||
|  | ): List<R> { | ||||||
|  |     val result = mutableListOf<R>() | ||||||
|  |     val iterator = iterator() | ||||||
|  |  | ||||||
|  |     prefix ?.let(result::add) | ||||||
|  |  | ||||||
|  |     while (iterator.hasNext()) { | ||||||
|  |         val element = iterator.next() | ||||||
|  |         result.add(transform(element) ?: continue) | ||||||
|  |  | ||||||
|  |         if (iterator.hasNext()) { | ||||||
|  |             result.add(separatorFun(element) ?: continue) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     postfix ?.let(result::add) | ||||||
|  |  | ||||||
|  |     return result | ||||||
|  | } | ||||||
|  |  | ||||||
|  | inline fun <I, R> Iterable<I>.joinTo( | ||||||
|  |     separator: R? = null, | ||||||
|  |     prefix: R? = null, | ||||||
|  |     postfix: R? = null, | ||||||
|  |     transform: (I) -> R? | ||||||
|  | ): List<R> = joinTo({ separator }, prefix, postfix, transform) | ||||||
|  |  | ||||||
|  | inline fun <I> Iterable<I>.joinTo( | ||||||
|  |     separatorFun: (I) -> I?, | ||||||
|  |     prefix: I? = null, | ||||||
|  |     postfix: I? = null | ||||||
|  | ): List<I> = joinTo<I, I>(separatorFun, prefix, postfix) { it } | ||||||
|  |  | ||||||
|  | inline fun <I> Iterable<I>.joinTo( | ||||||
|  |     separator: I? = null, | ||||||
|  |     prefix: I? = null, | ||||||
|  |     postfix: I? = null | ||||||
|  | ): List<I> = joinTo<I>({ separator }, prefix, postfix) | ||||||
|  |  | ||||||
|  | inline fun <I, reified R> Array<I>.joinTo( | ||||||
|  |     separatorFun: (I) -> R?, | ||||||
|  |     prefix: R? = null, | ||||||
|  |     postfix: R? = null, | ||||||
|  |     transform: (I) -> R? | ||||||
|  | ): Array<R> = asIterable().joinTo(separatorFun, prefix, postfix, transform).toTypedArray() | ||||||
|  |  | ||||||
|  | inline fun <I, reified R> Array<I>.joinTo( | ||||||
|  |     separator: R? = null, | ||||||
|  |     prefix: R? = null, | ||||||
|  |     postfix: R? = null, | ||||||
|  |     transform: (I) -> R? | ||||||
|  | ): Array<R> = asIterable().joinTo(separator, prefix, postfix, transform).toTypedArray() | ||||||
| @@ -0,0 +1,34 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | import kotlinx.serialization.Serializable | ||||||
|  | import kotlin.jvm.JvmInline | ||||||
|  |  | ||||||
|  | @Serializable | ||||||
|  | @JvmInline | ||||||
|  | value class FileName(val string: String) { | ||||||
|  |     val name: String | ||||||
|  |         get() = withoutSlashAtTheEnd.takeLastWhile { it != '/' } | ||||||
|  |     val extension: String | ||||||
|  |         get() = name.takeLastWhile { it != '.' } | ||||||
|  |     val nameWithoutExtension: String | ||||||
|  |         get() { | ||||||
|  |             val filename = name | ||||||
|  |             return filename.indexOfLast { it == '.' }.takeIf { it > -1 } ?.let { | ||||||
|  |                 filename.substring(0, it) | ||||||
|  |             } ?: filename | ||||||
|  |         } | ||||||
|  |     val withoutSlashAtTheEnd: String | ||||||
|  |         get() = string.dropLastWhile { it == '/' } | ||||||
|  |     override fun toString(): String = string | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | expect class MPPFile | ||||||
|  |  | ||||||
|  | expect val MPPFile.filename: FileName | ||||||
|  | expect val MPPFile.filesize: Long | ||||||
|  | expect val MPPFile.bytesAllocatorSync: ByteArrayAllocator | ||||||
|  | expect val MPPFile.bytesAllocator: SuspendByteArrayAllocator | ||||||
|  | fun MPPFile.bytesSync() = bytesAllocatorSync() | ||||||
|  | suspend fun MPPFile.bytes() = bytesAllocator() | ||||||
|  |  | ||||||
| @@ -0,0 +1,135 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Contains diff based on the comparison of objects with the same [K]. | ||||||
|  |  * | ||||||
|  |  * @param removed Contains map with keys removed from parent map | ||||||
|  |  * @param changed Contains map with keys values changed new map in comparison with old one | ||||||
|  |  * @param added Contains map with new keys and values | ||||||
|  |  */ | ||||||
|  | data class MapDiff<K, V> @Warning(warning) constructor( | ||||||
|  |     val removed: Map<K, V>, | ||||||
|  |     val changed: Map<K, Pair<V, V>>, | ||||||
|  |     val added: Map<K, V> | ||||||
|  | ) { | ||||||
|  |     fun isEmpty() = removed.isEmpty() && changed.isEmpty() && added.isEmpty() | ||||||
|  |     inline fun isNotEmpty() = !isEmpty() | ||||||
|  |  | ||||||
|  |     companion object { | ||||||
|  |         private const val warning = "This feature can be changed without any warranties. Use with caution and only in case you know what you are doing" | ||||||
|  |         fun <K, V> empty() = MapDiff<K, V>(emptyMap(), emptyMap(), emptyMap()) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | private inline fun <K, V> createCompareFun( | ||||||
|  |     strictComparison: Boolean | ||||||
|  | ): (K, V, V) -> Boolean = if (strictComparison) { | ||||||
|  |     { _, first, second -> first === second } | ||||||
|  | } else { | ||||||
|  |     { _, first, second -> first == second } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Compare [this] [Map] with the [other] one in principle when [other] is newer than [this] | ||||||
|  |  * | ||||||
|  |  * @param compareFun Will be used to determine changed values | ||||||
|  |  */ | ||||||
|  | fun <K, V> Map<K, V>.diff( | ||||||
|  |     other: Map<K, V>, | ||||||
|  |     compareFun: (K, V, V) -> Boolean | ||||||
|  | ): MapDiff<K, V> { | ||||||
|  |     val removed: Map<K, V> = (keys - other.keys).associateWith { | ||||||
|  |         getValue(it) | ||||||
|  |     } | ||||||
|  |     val added: Map<K, V> = (other.keys - keys).associateWith { | ||||||
|  |         other.getValue(it) | ||||||
|  |     } | ||||||
|  |     val changed = keys.intersect(other.keys).mapNotNull { | ||||||
|  |         val old = getValue(it) | ||||||
|  |         val new = other.getValue(it) | ||||||
|  |         if (compareFun(it, old, new)) { | ||||||
|  |             return@mapNotNull null | ||||||
|  |         } else { | ||||||
|  |             it to (old to new) | ||||||
|  |         } | ||||||
|  |     }.toMap() | ||||||
|  |  | ||||||
|  |     return MapDiff( | ||||||
|  |         removed, | ||||||
|  |         changed, | ||||||
|  |         added | ||||||
|  |     ) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Compare [this] [Map] with the [other] one in principle when [other] is newer than [this] | ||||||
|  |  * | ||||||
|  |  * @param strictComparison If true, will use strict (===) comparison for the values' comparison. Otherwise, standard | ||||||
|  |  * `equals` will be used | ||||||
|  |  */ | ||||||
|  | fun <K, V> Map<K, V>.diff( | ||||||
|  |     other: Map<K, V>, | ||||||
|  |     strictComparison: Boolean = false | ||||||
|  | ): MapDiff<K, V> = diff( | ||||||
|  |     other, | ||||||
|  |     compareFun = createCompareFun(strictComparison) | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Will apply [mapDiff] to [this] [MutableMap] | ||||||
|  |  */ | ||||||
|  | fun <K, V> MutableMap<K, V>.applyDiff( | ||||||
|  |     mapDiff: MapDiff<K, V> | ||||||
|  | ) { | ||||||
|  |     mapDiff.apply { | ||||||
|  |         removed.keys.forEach { remove(it) } | ||||||
|  |         changed.forEach { (k, oldNew) -> | ||||||
|  |             put(k, oldNew.second) | ||||||
|  |         } | ||||||
|  |         added.forEach { (k, new) -> | ||||||
|  |             put(k, new) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Will apply changes with [from] map into [this] one | ||||||
|  |  * | ||||||
|  |  * @param compareFun Will be used to determine changed values | ||||||
|  |  * | ||||||
|  |  * @return [MapDiff] applied to [this] [MutableMap] | ||||||
|  |  */ | ||||||
|  | fun <K, V> MutableMap<K, V>.applyDiff( | ||||||
|  |     from: Map<K, V>, | ||||||
|  |     compareFun: (K, V, V) -> Boolean | ||||||
|  | ): MapDiff<K, V> { | ||||||
|  |     return diff(from, compareFun).also { | ||||||
|  |         applyDiff(it) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Will apply changes with [from] map into [this] one | ||||||
|  |  * | ||||||
|  |  * @param strictComparison If true, will use strict (===) comparison for the values' comparison. Otherwise, standard | ||||||
|  |  * `equals` will be used | ||||||
|  |  * | ||||||
|  |  * @return [MapDiff] applied to [this] [MutableMap] | ||||||
|  |  */ | ||||||
|  | fun <K, V> MutableMap<K, V>.applyDiff( | ||||||
|  |     from: Map<K, V>, | ||||||
|  |     strictComparison: Boolean = false | ||||||
|  | ): MapDiff<K, V> = applyDiff( | ||||||
|  |     from, | ||||||
|  |     compareFun = createCompareFun(strictComparison) | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Reverse [this] [MapDiff]. Result will contain [MapDiff.added] on [MapDiff.removed] (and vice-verse), all the | ||||||
|  |  * [MapDiff.changed] values will be reversed too | ||||||
|  |  */ | ||||||
|  | fun <K, V> MapDiff<K, V>.reversed(): MapDiff<K, V> = MapDiff( | ||||||
|  |     removed = added, | ||||||
|  |     changed = changed.mapValues { (_, oldNew) -> oldNew.second to oldNew.first }, | ||||||
|  |     added = removed | ||||||
|  | ) | ||||||
| @@ -0,0 +1,53 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | import kotlin.jvm.JvmName | ||||||
|  |  | ||||||
|  | interface SimpleMapper<T1, T2> { | ||||||
|  |     fun convertToT1(from: T2): T1 | ||||||
|  |     fun convertToT2(from: T1): T2 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @JvmName("convertFromT2") | ||||||
|  | fun <T1, T2> SimpleMapper<T1, T2>.convert(from: T2) = convertToT1(from) | ||||||
|  | @JvmName("convertFromT1") | ||||||
|  | fun <T1, T2> SimpleMapper<T1, T2>.convert(from: T1) = convertToT2(from) | ||||||
|  |  | ||||||
|  | class SimpleMapperImpl<T1, T2>( | ||||||
|  |     private val t1: (T2) -> T1, | ||||||
|  |     private val t2: (T1) -> T2, | ||||||
|  | ) : SimpleMapper<T1, T2> { | ||||||
|  |     override fun convertToT1(from: T2): T1 = t1.invoke(from) | ||||||
|  |  | ||||||
|  |     override fun convertToT2(from: T1): T2 = t2.invoke(from) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @Suppress("NOTHING_TO_INLINE") | ||||||
|  | inline fun <T1, T2> simpleMapper( | ||||||
|  |     noinline t1: (T2) -> T1, | ||||||
|  |     noinline t2: (T1) -> T2, | ||||||
|  | ) = SimpleMapperImpl(t1, t2) | ||||||
|  |  | ||||||
|  | interface SimpleSuspendableMapper<T1, T2> { | ||||||
|  |     suspend fun convertToT1(from: T2): T1 | ||||||
|  |     suspend fun convertToT2(from: T1): T2 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @JvmName("convertFromT2") | ||||||
|  | suspend fun <T1, T2> SimpleSuspendableMapper<T1, T2>.convert(from: T2) = convertToT1(from) | ||||||
|  | @JvmName("convertFromT1") | ||||||
|  | suspend fun <T1, T2> SimpleSuspendableMapper<T1, T2>.convert(from: T1) = convertToT2(from) | ||||||
|  |  | ||||||
|  | class SimpleSuspendableMapperImpl<T1, T2>( | ||||||
|  |     private val t1: suspend (T2) -> T1, | ||||||
|  |     private val t2: suspend (T1) -> T2, | ||||||
|  | ) : SimpleSuspendableMapper<T1, T2> { | ||||||
|  |     override suspend fun convertToT1(from: T2): T1 = t1.invoke(from) | ||||||
|  |  | ||||||
|  |     override suspend fun convertToT2(from: T1): T2 = t2.invoke(from) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @Suppress("NOTHING_TO_INLINE") | ||||||
|  | inline fun <T1, T2> simpleSuspendableMapper( | ||||||
|  |     noinline t1: suspend (T2) -> T1, | ||||||
|  |     noinline t2: suspend (T1) -> T2, | ||||||
|  | ) = SimpleSuspendableMapperImpl(t1, t2) | ||||||
| @@ -0,0 +1,97 @@ | |||||||
|  | @file:Suppress("unused") | ||||||
|  |  | ||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | import kotlinx.serialization.Serializable | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * This type represents [T] as not only potentially nullable data, but also as a data which can not be presented. This | ||||||
|  |  * type will be useful in cases when [T] is nullable and null as valuable data too in time of data absence should be | ||||||
|  |  * presented by some third type. | ||||||
|  |  * | ||||||
|  |  * Let's imagine, you have nullable name in some database. In case when name is not nullable everything is clear - null | ||||||
|  |  * will represent absence of row in the database. In case when name is nullable null will be a little bit dual-meaning, | ||||||
|  |  * cause this null will say nothing about availability of the row (of course, it is exaggerated example) | ||||||
|  |  * | ||||||
|  |  * @see Optional.presented | ||||||
|  |  * @see Optional.absent | ||||||
|  |  * @see Optional.optional | ||||||
|  |  * @see Optional.onPresented | ||||||
|  |  * @see Optional.onAbsent | ||||||
|  |  */ | ||||||
|  | @Serializable | ||||||
|  | data class Optional<T> internal constructor( | ||||||
|  |     @Warning("It is unsafe to use this data directly") | ||||||
|  |     val data: T?, | ||||||
|  |     @Warning("It is unsafe to use this data directly") | ||||||
|  |     val dataPresented: Boolean | ||||||
|  | ) { | ||||||
|  |     companion object { | ||||||
|  |         /** | ||||||
|  |          * Will create [Optional] with presented data | ||||||
|  |          */ | ||||||
|  |         fun <T> presented(data: T) = Optional(data, true) | ||||||
|  |         /** | ||||||
|  |          * Will create [Optional] without data | ||||||
|  |          */ | ||||||
|  |         fun <T> absent() = Optional<T>(null, false) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | inline val <T> T.optional | ||||||
|  |     get() = Optional.presented(this) | ||||||
|  |  | ||||||
|  | inline val <T : Any> T?.optionalOrAbsentIfNull | ||||||
|  |     get() = if (this == null) { | ||||||
|  |         Optional.absent<T>() | ||||||
|  |     } else { | ||||||
|  |         Optional.presented(this) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Will call [block] when data presented ([Optional.dataPresented] == true) | ||||||
|  |  */ | ||||||
|  | inline fun <T> Optional<T>.onPresented(block: (T) -> Unit): Optional<T> = apply { | ||||||
|  |     @OptIn(Warning::class) | ||||||
|  |     if (dataPresented) { @Suppress("UNCHECKED_CAST") block(data as T) } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Will call [block] when data presented ([Optional.dataPresented] == true) | ||||||
|  |  */ | ||||||
|  | inline fun <T, R> Optional<T>.mapOnPresented(block: (T) -> R): R? = run { | ||||||
|  |     @OptIn(Warning::class) | ||||||
|  |     if (dataPresented) { @Suppress("UNCHECKED_CAST") block(data as T) } else null | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Will call [block] when data absent ([Optional.dataPresented] == false) | ||||||
|  |  */ | ||||||
|  | inline fun <T> Optional<T>.onAbsent(block: () -> Unit): Optional<T> = apply { | ||||||
|  |     @OptIn(Warning::class) | ||||||
|  |     if (!dataPresented) { block() } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Will call [block] when data presented ([Optional.dataPresented] == true) | ||||||
|  |  */ | ||||||
|  | inline fun <T, R> Optional<T>.mapOnAbsent(block: () -> R): R? = run { | ||||||
|  |     @OptIn(Warning::class) | ||||||
|  |     if (!dataPresented) { block() } else null | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Returns [Optional.data] if [Optional.dataPresented] of [this] is true, or null otherwise | ||||||
|  |  */ | ||||||
|  | fun <T> Optional<T>.dataOrNull() = @OptIn(Warning::class) if (dataPresented) @Suppress("UNCHECKED_CAST") (data as T) else null | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Returns [Optional.data] if [Optional.dataPresented] of [this] is true, or throw [throwable] otherwise | ||||||
|  |  */ | ||||||
|  | fun <T> Optional<T>.dataOrThrow(throwable: Throwable) = @OptIn(Warning::class) if (dataPresented) @Suppress("UNCHECKED_CAST") (data as T) else throw throwable | ||||||
|  |  | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Returns [Optional.data] if [Optional.dataPresented] of [this] is true, or call [block] and returns the result of it | ||||||
|  |  */ | ||||||
|  | inline fun <T> Optional<T>.dataOrElse(block: () -> T) = @OptIn(Warning::class) if (dataPresented) @Suppress("UNCHECKED_CAST") (data as T) else block() | ||||||
| @@ -0,0 +1,28 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Convert [this] [Long] to [Int] with bounds of [Int.MIN_VALUE] and [Int.MAX_VALUE] | ||||||
|  |  */ | ||||||
|  | fun Long.toCoercedInt(): Int = coerceIn(Int.MIN_VALUE.toLong(), Int.MAX_VALUE.toLong()).toInt() | ||||||
|  | /** | ||||||
|  |  * Convert [this] [Long] to [Short] with bounds of [Short.MIN_VALUE] and [Short.MAX_VALUE] | ||||||
|  |  */ | ||||||
|  | fun Long.toCoercedShort(): Short = coerceIn(Short.MIN_VALUE.toLong(), Short.MAX_VALUE.toLong()).toShort() | ||||||
|  | /** | ||||||
|  |  * Convert [this] [Long] to [Byte] with bounds of [Byte.MIN_VALUE] and [Byte.MAX_VALUE] | ||||||
|  |  */ | ||||||
|  | fun Long.toCoercedByte(): Byte = coerceIn(Byte.MIN_VALUE.toLong(), Byte.MAX_VALUE.toLong()).toByte() | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Convert [this] [Int] to [Short] with bounds of [Short.MIN_VALUE] and [Short.MAX_VALUE] | ||||||
|  |  */ | ||||||
|  | fun Int.toCoercedShort(): Short = coerceIn(Short.MIN_VALUE.toInt(), Short.MAX_VALUE.toInt()).toShort() | ||||||
|  | /** | ||||||
|  |  * Convert [this] [Int] to [Byte] with bounds of [Byte.MIN_VALUE] and [Byte.MAX_VALUE] | ||||||
|  |  */ | ||||||
|  | fun Int.toCoercedByte(): Byte = coerceIn(Byte.MIN_VALUE.toInt(), Byte.MAX_VALUE.toInt()).toByte() | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Convert [this] [Short] to [Byte] with bounds of [Byte.MIN_VALUE] and [Byte.MAX_VALUE] | ||||||
|  |  */ | ||||||
|  | fun Short.toCoercedByte(): Byte = coerceIn(Byte.MIN_VALUE.toShort(), Byte.MAX_VALUE.toShort()).toByte() | ||||||
| @@ -0,0 +1,37 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | import kotlinx.serialization.Serializable | ||||||
|  | import kotlin.jvm.JvmInline | ||||||
|  |  | ||||||
|  | @Serializable | ||||||
|  | @JvmInline | ||||||
|  | value class Progress private constructor( | ||||||
|  |     val of1: Double | ||||||
|  | ) { | ||||||
|  |     val of1Float | ||||||
|  |         get() = of1.toFloat() | ||||||
|  |     val of100 | ||||||
|  |         get() = of1 * 100 | ||||||
|  |     val of100Float | ||||||
|  |         get() = of100.toFloat() | ||||||
|  |     val of100Int | ||||||
|  |         get() = of100.toInt() | ||||||
|  |  | ||||||
|  |     init { | ||||||
|  |         require(of1 in rangeOfValues) { | ||||||
|  |             "Progress main value should be in $rangeOfValues, but incoming value is $of1" | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     companion object { | ||||||
|  |         val rangeOfValues = 0.0 .. 1.0 | ||||||
|  |  | ||||||
|  |         val START = Progress(rangeOfValues.start) | ||||||
|  |         val COMPLETED = Progress(rangeOfValues.endInclusive) | ||||||
|  |  | ||||||
|  |         operator fun invoke(of1: Double) = Progress(of1.coerceIn(rangeOfValues)) | ||||||
|  |         operator fun invoke(part: Number, total: Number) = Progress( | ||||||
|  |             part.toDouble() / total.toDouble() | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,80 @@ | |||||||
|  | @file:Suppress( | ||||||
|  |   "RemoveRedundantCallsOfConversionMethods", | ||||||
|  |   "RedundantVisibilityModifier", | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | import kotlin.Byte | ||||||
|  | import kotlin.Double | ||||||
|  | import kotlin.Float | ||||||
|  | import kotlin.Int | ||||||
|  | import kotlin.Long | ||||||
|  | import kotlin.Short | ||||||
|  | import kotlin.Suppress | ||||||
|  |  | ||||||
|  | public operator fun Progress.plus(other: Progress): Progress = Progress(of1 + other.of1) | ||||||
|  |  | ||||||
|  | public operator fun Progress.minus(other: Progress): Progress = Progress(of1 - other.of1) | ||||||
|  |  | ||||||
|  | public operator fun Progress.plus(i: Byte): Progress = Progress((of1 + i).toDouble()) | ||||||
|  |  | ||||||
|  | public operator fun Progress.minus(i: Byte): Progress = Progress((of1 - i).toDouble()) | ||||||
|  |  | ||||||
|  | public operator fun Progress.times(i: Byte): Progress = Progress((of1 * i).toDouble()) | ||||||
|  |  | ||||||
|  | public operator fun Progress.div(i: Byte): Progress = Progress((of1 / i).toDouble()) | ||||||
|  |  | ||||||
|  | public operator fun Progress.rem(i: Byte): Progress = Progress((of1 % i).toDouble()) | ||||||
|  |  | ||||||
|  | public operator fun Progress.plus(i: Short): Progress = Progress((of1 + i).toDouble()) | ||||||
|  |  | ||||||
|  | public operator fun Progress.minus(i: Short): Progress = Progress((of1 - i).toDouble()) | ||||||
|  |  | ||||||
|  | public operator fun Progress.times(i: Short): Progress = Progress((of1 * i).toDouble()) | ||||||
|  |  | ||||||
|  | public operator fun Progress.div(i: Short): Progress = Progress((of1 / i).toDouble()) | ||||||
|  |  | ||||||
|  | public operator fun Progress.rem(i: Short): Progress = Progress((of1 % i).toDouble()) | ||||||
|  |  | ||||||
|  | public operator fun Progress.plus(i: Int): Progress = Progress((of1 + i).toDouble()) | ||||||
|  |  | ||||||
|  | public operator fun Progress.minus(i: Int): Progress = Progress((of1 - i).toDouble()) | ||||||
|  |  | ||||||
|  | public operator fun Progress.times(i: Int): Progress = Progress((of1 * i).toDouble()) | ||||||
|  |  | ||||||
|  | public operator fun Progress.div(i: Int): Progress = Progress((of1 / i).toDouble()) | ||||||
|  |  | ||||||
|  | public operator fun Progress.rem(i: Int): Progress = Progress((of1 % i).toDouble()) | ||||||
|  |  | ||||||
|  | public operator fun Progress.plus(i: Long): Progress = Progress((of1 + i).toDouble()) | ||||||
|  |  | ||||||
|  | public operator fun Progress.minus(i: Long): Progress = Progress((of1 - i).toDouble()) | ||||||
|  |  | ||||||
|  | public operator fun Progress.times(i: Long): Progress = Progress((of1 * i).toDouble()) | ||||||
|  |  | ||||||
|  | public operator fun Progress.div(i: Long): Progress = Progress((of1 / i).toDouble()) | ||||||
|  |  | ||||||
|  | public operator fun Progress.rem(i: Long): Progress = Progress((of1 % i).toDouble()) | ||||||
|  |  | ||||||
|  | public operator fun Progress.plus(i: Float): Progress = Progress((of1 + i).toDouble()) | ||||||
|  |  | ||||||
|  | public operator fun Progress.minus(i: Float): Progress = Progress((of1 - i).toDouble()) | ||||||
|  |  | ||||||
|  | public operator fun Progress.times(i: Float): Progress = Progress((of1 * i).toDouble()) | ||||||
|  |  | ||||||
|  | public operator fun Progress.div(i: Float): Progress = Progress((of1 / i).toDouble()) | ||||||
|  |  | ||||||
|  | public operator fun Progress.rem(i: Float): Progress = Progress((of1 % i).toDouble()) | ||||||
|  |  | ||||||
|  | public operator fun Progress.plus(i: Double): Progress = Progress((of1 + i).toDouble()) | ||||||
|  |  | ||||||
|  | public operator fun Progress.minus(i: Double): Progress = Progress((of1 - i).toDouble()) | ||||||
|  |  | ||||||
|  | public operator fun Progress.times(i: Double): Progress = Progress((of1 * i).toDouble()) | ||||||
|  |  | ||||||
|  | public operator fun Progress.div(i: Double): Progress = Progress((of1 / i).toDouble()) | ||||||
|  |  | ||||||
|  | public operator fun Progress.rem(i: Double): Progress = Progress((of1 % i).toDouble()) | ||||||
|  |  | ||||||
|  | public operator fun Progress.compareTo(other: Progress): Int = (of1 - other.of1).toInt() | ||||||
| @@ -0,0 +1,19 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | fun <T : Comparable<T>> ClosedRange<T>.intersect(other: ClosedRange<T>): Pair<T, T>? = when { | ||||||
|  |     start == other.start && endInclusive == other.endInclusive -> start to endInclusive | ||||||
|  |     start > other.endInclusive || other.start > endInclusive -> null | ||||||
|  |     else -> maxOf(start, other.start) to minOf(endInclusive, other.endInclusive) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fun IntRange.intersect( | ||||||
|  |     other: IntRange | ||||||
|  | ): IntRange? = (this as ClosedRange<Int>).intersect(other as ClosedRange<Int>) ?.let { | ||||||
|  |     it.first .. it.second | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fun LongRange.intersect( | ||||||
|  |     other: LongRange | ||||||
|  | ): LongRange? = (this as ClosedRange<Long>).intersect(other as ClosedRange<Long>) ?.let { | ||||||
|  |     it.first .. it.second | ||||||
|  | } | ||||||
| @@ -0,0 +1,54 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Executes the given [action] until getting of successful result specified number of [times]. | ||||||
|  |  * | ||||||
|  |  * A zero-based index of current iteration is passed as a parameter to [action]. | ||||||
|  |  */ | ||||||
|  | inline fun <R> repeatOnFailure( | ||||||
|  |     onFailure: (Throwable) -> Boolean, | ||||||
|  |     action: () -> R | ||||||
|  | ): Result<R> { | ||||||
|  |     do { | ||||||
|  |         runCatching { | ||||||
|  |             action() | ||||||
|  |         }.onFailure { | ||||||
|  |             if (!onFailure(it)) { | ||||||
|  |                 return Result.failure(it) | ||||||
|  |             } | ||||||
|  |         }.onSuccess { | ||||||
|  |             return Result.success(it) | ||||||
|  |         } | ||||||
|  |     } while (true) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Executes the given [action] until getting of successful result specified number of [times]. | ||||||
|  |  * | ||||||
|  |  * A zero-based index of current iteration is passed as a parameter to [action]. | ||||||
|  |  */ | ||||||
|  | inline fun <R> repeatOnFailure( | ||||||
|  |     times: Int, | ||||||
|  |     onEachFailure: (Throwable) -> Unit = {}, | ||||||
|  |     action: (Int) -> R | ||||||
|  | ): Optional<R> { | ||||||
|  |     var i = 0 | ||||||
|  |     val result = repeatOnFailure( | ||||||
|  |         { | ||||||
|  |             onEachFailure(it) | ||||||
|  |             if (i < times) { | ||||||
|  |                 i++ | ||||||
|  |                 true | ||||||
|  |             } else { | ||||||
|  |                 false | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     ) { | ||||||
|  |         action(i) | ||||||
|  |     } | ||||||
|  |     return if (result.isSuccess) { | ||||||
|  |         Optional.presented(result.getOrThrow()) | ||||||
|  |     } else { | ||||||
|  |         Optional.absent() | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,6 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | val FixedSignsRange = 0 .. 100 | ||||||
|  |  | ||||||
|  | expect fun Float.fixed(signs: Int): Float | ||||||
|  | expect fun Double.fixed(signs: Int): Double | ||||||
| @@ -11,7 +11,7 @@ class DiffUtilsTests { | |||||||
|         val withIndex = oldList.withIndex() |         val withIndex = oldList.withIndex() | ||||||
|  |  | ||||||
|         for (count in 1 .. (floor(oldList.size.toFloat() / 2).toInt())) { |         for (count in 1 .. (floor(oldList.size.toFloat() / 2).toInt())) { | ||||||
|             for ((i, v) in withIndex) { |             for ((i, _) in withIndex) { | ||||||
|                 if (i + count > oldList.lastIndex) { |                 if (i + count > oldList.lastIndex) { | ||||||
|                     continue |                     continue | ||||||
|                 } |                 } | ||||||
| @@ -32,7 +32,7 @@ class DiffUtilsTests { | |||||||
|         val withIndex = oldList.withIndex() |         val withIndex = oldList.withIndex() | ||||||
|  |  | ||||||
|         for (count in 1 .. (floor(oldList.size.toFloat() / 2).toInt())) { |         for (count in 1 .. (floor(oldList.size.toFloat() / 2).toInt())) { | ||||||
|             for ((i, v) in withIndex) { |             for ((i, _) in withIndex) { | ||||||
|                 if (i + count > oldList.lastIndex) { |                 if (i + count > oldList.lastIndex) { | ||||||
|                     continue |                     continue | ||||||
|                 } |                 } | ||||||
| @@ -54,8 +54,8 @@ class DiffUtilsTests { | |||||||
|         val oldList = (0 until 10).map { it.toString() } |         val oldList = (0 until 10).map { it.toString() } | ||||||
|         val withIndex = oldList.withIndex() |         val withIndex = oldList.withIndex() | ||||||
|  |  | ||||||
|         for (step in 0 until oldList.size) { |         for (step in oldList.indices) { | ||||||
|             for ((i, v) in withIndex) { |             for ((i, _) in withIndex) { | ||||||
|                 val mutable = oldList.toMutableList() |                 val mutable = oldList.toMutableList() | ||||||
|                 val changes = ( |                 val changes = ( | ||||||
|                     if (step == 0) i until oldList.size else (i until oldList.size step step) |                     if (step == 0) i until oldList.size else (i until oldList.size step step) | ||||||
| @@ -73,4 +73,83 @@ class DiffUtilsTests { | |||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     @Test | ||||||
|  |     fun testThatSimpleRemoveApplyWorks() { | ||||||
|  |         val oldList = (0 until 10).toList() | ||||||
|  |         val withIndex = oldList.withIndex() | ||||||
|  |  | ||||||
|  |         for (count in 1 .. (floor(oldList.size.toFloat() / 2).toInt())) { | ||||||
|  |             for ((i, _) in withIndex) { | ||||||
|  |                 if (i + count > oldList.lastIndex) { | ||||||
|  |                     continue | ||||||
|  |                 } | ||||||
|  |                 val removedSublist = oldList.subList(i, i + count) | ||||||
|  |                 val mutableOldList = oldList.toMutableList() | ||||||
|  |                 val targetList = oldList - removedSublist | ||||||
|  |  | ||||||
|  |                 mutableOldList.applyDiff(targetList) | ||||||
|  |  | ||||||
|  |                 assertEquals( | ||||||
|  |                     targetList, | ||||||
|  |                     mutableOldList | ||||||
|  |                 ) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Test | ||||||
|  |     fun testThatSimpleAddApplyWorks() { | ||||||
|  |         val oldList = (0 until 10).map { it.toString() } | ||||||
|  |         val withIndex = oldList.withIndex() | ||||||
|  |  | ||||||
|  |         for (count in 1 .. (floor(oldList.size.toFloat() / 2).toInt())) { | ||||||
|  |             for ((i, _) in withIndex) { | ||||||
|  |                 if (i + count > oldList.lastIndex) { | ||||||
|  |                     continue | ||||||
|  |                 } | ||||||
|  |                 val addedSublist = oldList.subList(i, i + count).map { "added$it" } | ||||||
|  |                 val mutable = oldList.toMutableList() | ||||||
|  |                 mutable.addAll(i, addedSublist) | ||||||
|  |                 val mutableOldList = oldList.toMutableList() | ||||||
|  |  | ||||||
|  |                 mutableOldList.applyDiff(mutable) | ||||||
|  |  | ||||||
|  |                 assertEquals( | ||||||
|  |                     mutable, | ||||||
|  |                     mutableOldList | ||||||
|  |                 ) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Test | ||||||
|  |     fun testThatSimpleChangesApplyWorks() { | ||||||
|  |         val oldList = (0 until 10).map { it.toString() } | ||||||
|  |         val withIndex = oldList.withIndex() | ||||||
|  |  | ||||||
|  |         for (step in oldList.indices) { | ||||||
|  |             for ((i, _) in withIndex) { | ||||||
|  |                 val mutable = oldList.toMutableList() | ||||||
|  |  | ||||||
|  |                 val newList = if (step == 0) { | ||||||
|  |                     i until oldList.size | ||||||
|  |                 } else { | ||||||
|  |                     i until oldList.size step step | ||||||
|  |                 } | ||||||
|  |                 newList.forEach { index -> | ||||||
|  |                     IndexedValue(index, mutable[index]) to IndexedValue(index, "changed$index").also { | ||||||
|  |                         mutable[index] = it.value | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 val mutableOldList = oldList.toMutableList() | ||||||
|  |                 mutableOldList.applyDiff(mutable) | ||||||
|  |                 assertEquals( | ||||||
|  |                     mutable, | ||||||
|  |                     mutableOldList | ||||||
|  |                 ) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -0,0 +1,15 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | import org.khronos.webgl.* | ||||||
|  |  | ||||||
|  | fun DataView.toByteArray() = ByteArray(this.byteLength) { | ||||||
|  |     getInt8(it) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fun ArrayBuffer.toByteArray() = Int8Array(this) as ByteArray | ||||||
|  |  | ||||||
|  | fun ByteArray.toDataView() = DataView(ArrayBuffer(size)).also { | ||||||
|  |     forEachIndexed { i, byte -> it.setInt8(i, byte) } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fun ByteArray.toArrayBuffer() = toDataView().buffer | ||||||
| @@ -0,0 +1,61 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | import kotlinx.browser.document | ||||||
|  | import org.w3c.dom.* | ||||||
|  |  | ||||||
|  | fun Node.onRemoved(block: () -> Unit): MutationObserver { | ||||||
|  |     lateinit var observer: MutationObserver | ||||||
|  |  | ||||||
|  |     observer = MutationObserver { _, _ -> | ||||||
|  |         fun checkIfRemoved(node: Node): Boolean { | ||||||
|  |             return node.parentNode != document && (node.parentNode ?.let { checkIfRemoved(it) } ?: true) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (checkIfRemoved(this)) { | ||||||
|  |             observer.disconnect() | ||||||
|  |             block() | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     observer.observe(document, MutationObserverInit(childList = true, subtree = true)) | ||||||
|  |     return observer | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fun Element.onVisibilityChanged(block: IntersectionObserverEntry.(Float, IntersectionObserver) -> Unit): IntersectionObserver { | ||||||
|  |     var previousIntersectionRatio = -1f | ||||||
|  |     val observer = IntersectionObserver { entries, observer -> | ||||||
|  |         entries.forEach { | ||||||
|  |             if (previousIntersectionRatio != it.intersectionRatio) { | ||||||
|  |                 previousIntersectionRatio = it.intersectionRatio.toFloat() | ||||||
|  |                 it.block(previousIntersectionRatio, observer) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     observer.observe(this) | ||||||
|  |     return observer | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fun Element.onVisible(block: Element.(IntersectionObserver) -> Unit) { | ||||||
|  |     var previous = -1f | ||||||
|  |     onVisibilityChanged { intersectionRatio, observer -> | ||||||
|  |         if (previous != intersectionRatio) { | ||||||
|  |             if (intersectionRatio > 0 && previous == 0f) { | ||||||
|  |                 block(observer) | ||||||
|  |             } | ||||||
|  |             previous = intersectionRatio | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fun Element.onInvisible(block: Element.(IntersectionObserver) -> Unit): IntersectionObserver { | ||||||
|  |     var previous = -1f | ||||||
|  |     return onVisibilityChanged { intersectionRatio, observer -> | ||||||
|  |         if (previous != intersectionRatio) { | ||||||
|  |             if (intersectionRatio == 0f && previous != 0f) { | ||||||
|  |                 block(observer) | ||||||
|  |             } | ||||||
|  |             previous = intersectionRatio | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,124 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | import org.w3c.dom.DOMRectReadOnly | ||||||
|  | import org.w3c.dom.Element | ||||||
|  |  | ||||||
|  | external interface IntersectionObserverOptions { | ||||||
|  |     /** | ||||||
|  |      * An Element or Document object which is an ancestor of the intended target, whose bounding rectangle will be | ||||||
|  |      * considered the viewport. Any part of the target not visible in the visible area of the root is not considered | ||||||
|  |      * visible. | ||||||
|  |      */ | ||||||
|  |     var root: Element? | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * A string which specifies a set of offsets to add to the root's bounding_box when calculating intersections, | ||||||
|  |      * effectively shrinking or growing the root for calculation purposes. The syntax is approximately the same as that | ||||||
|  |      * for the CSS margin property; see The root element and root margin in Intersection Observer API for more | ||||||
|  |      * information on how the margin works and the syntax. The default is "0px 0px 0px 0px". | ||||||
|  |      */ | ||||||
|  |     var rootMargin: String? | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Either a single number or an array of numbers between 0.0 and 1.0, specifying a ratio of intersection area to | ||||||
|  |      * total bounding box area for the observed target. A value of 0.0 means that even a single visible pixel counts as | ||||||
|  |      * the target being visible. 1.0 means that the entire target element is visible. See Thresholds in Intersection | ||||||
|  |      * Observer API for a more in-depth description of how thresholds are used. The default is a threshold of 0.0. | ||||||
|  |      */ | ||||||
|  |     var threshold: Array<Number>? | ||||||
|  | } | ||||||
|  | fun IntersectionObserverOptions( | ||||||
|  |     block: IntersectionObserverOptions.() -> Unit = {} | ||||||
|  | ): IntersectionObserverOptions = js("{}").unsafeCast<IntersectionObserverOptions>().apply(block) | ||||||
|  |  | ||||||
|  | external interface IntersectionObserverEntry { | ||||||
|  |     /** | ||||||
|  |      * Returns the bounds rectangle of the target element as a DOMRectReadOnly. The bounds are computed as described in | ||||||
|  |      * the documentation for Element.getBoundingClientRect(). | ||||||
|  |      */ | ||||||
|  |     val boundingClientRect: DOMRectReadOnly | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Returns the ratio of the intersectionRect to the boundingClientRect. | ||||||
|  |      */ | ||||||
|  |     val intersectionRatio: Number | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Returns a DOMRectReadOnly representing the target's visible area. | ||||||
|  |      */ | ||||||
|  |     val intersectionRect: DOMRectReadOnly | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * A Boolean value which is true if the target element intersects with the intersection observer's root. If this is | ||||||
|  |      * true, then, the IntersectionObserverEntry describes a transition into a state of intersection; if it's false, | ||||||
|  |      * then you know the transition is from intersecting to not-intersecting. | ||||||
|  |      */ | ||||||
|  |     val isIntersecting: Boolean | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Returns a DOMRectReadOnly for the intersection observer's root. | ||||||
|  |      */ | ||||||
|  |     val rootBounds: DOMRectReadOnly | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * The Element whose intersection with the root changed. | ||||||
|  |      */ | ||||||
|  |     val target: Element | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * A DOMHighResTimeStamp indicating the time at which the intersection was recorded, relative to the | ||||||
|  |      * IntersectionObserver's time origin. | ||||||
|  |      */ | ||||||
|  |     val time: Double | ||||||
|  | } | ||||||
|  |  | ||||||
|  | typealias IntersectionObserverCallback = (entries: Array<IntersectionObserverEntry>, observer: IntersectionObserver) -> Unit | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * This is just an implementation from [this commentary](https://youtrack.jetbrains.com/issue/KT-43157#focus=Comments-27-4498582.0-0) | ||||||
|  |  * of Kotlin JS issue related to the absence of [Intersection Observer API](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver) | ||||||
|  |  */ | ||||||
|  | external class IntersectionObserver(callback: IntersectionObserverCallback) { | ||||||
|  |     constructor(callback: IntersectionObserverCallback, options: IntersectionObserverOptions) | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * The Element or Document whose bounds are used as the bounding box when testing for intersection. If no root value | ||||||
|  |      * was passed to the constructor or its value is null, the top-level document's viewport is used. | ||||||
|  |      */ | ||||||
|  |     val root: Element | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * An offset rectangle applied to the root's bounding box when calculating intersections, effectively shrinking or | ||||||
|  |      * growing the root for calculation purposes. The value returned by this property may not be the same as the one | ||||||
|  |      * specified when calling the constructor as it may be changed to match internal requirements. Each offset can be | ||||||
|  |      * expressed in pixels (px) or as a percentage (%). The default is "0px 0px 0px 0px". | ||||||
|  |      */ | ||||||
|  |     val rootMargin: String | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * A list of thresholds, sorted in increasing numeric order, where each threshold is a ratio of intersection area to | ||||||
|  |      * bounding box area of an observed target. Notifications for a target are generated when any of the thresholds are | ||||||
|  |      * crossed for that target. If no value was passed to the constructor, 0 is used. | ||||||
|  |      */ | ||||||
|  |     val thresholds: Array<Number> | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Stops the IntersectionObserver object from observing any target. | ||||||
|  |      */ | ||||||
|  |     fun disconnect() | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Tells the IntersectionObserver a target element to observe. | ||||||
|  |      */ | ||||||
|  |     fun observe(targetElement: Element) | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Returns an array of IntersectionObserverEntry objects for all observed targets. | ||||||
|  |      */ | ||||||
|  |     fun takeRecords(): Array<IntersectionObserverEntry> | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Tells the IntersectionObserver to stop observing a particular target element. | ||||||
|  |      */ | ||||||
|  |     fun unobserve(targetElement: Element) | ||||||
|  | } | ||||||
| @@ -0,0 +1,12 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | import org.w3c.dom.Element | ||||||
|  |  | ||||||
|  | inline val Element.isOverflowWidth | ||||||
|  |     get() = scrollWidth > clientWidth | ||||||
|  |  | ||||||
|  | inline val Element.isOverflowHeight | ||||||
|  |     get() = scrollHeight > clientHeight | ||||||
|  |  | ||||||
|  | inline val Element.isOverflow | ||||||
|  |     get() = isOverflowHeight || isOverflowWidth | ||||||
| @@ -0,0 +1,54 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | import org.khronos.webgl.ArrayBuffer | ||||||
|  | import org.w3c.dom.ErrorEvent | ||||||
|  | import org.w3c.files.* | ||||||
|  | import kotlin.js.Promise | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @suppress | ||||||
|  |  */ | ||||||
|  | actual typealias MPPFile = File | ||||||
|  |  | ||||||
|  | fun MPPFile.readBytesPromise() = Promise<ByteArray> { success, failure -> | ||||||
|  |     val reader = FileReader() | ||||||
|  |     reader.onload = { | ||||||
|  |         success((reader.result as ArrayBuffer).toByteArray()) | ||||||
|  |         Unit | ||||||
|  |     } | ||||||
|  |     reader.onerror = { | ||||||
|  |         failure(Exception((it as ErrorEvent).message)) | ||||||
|  |         Unit | ||||||
|  |     } | ||||||
|  |     reader.readAsArrayBuffer(this) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fun MPPFile.readBytes(): ByteArray { | ||||||
|  |     val reader = FileReaderSync() | ||||||
|  |     return reader.readAsArrayBuffer(this).toByteArray() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | private suspend fun MPPFile.dirtyReadBytes(): ByteArray = readBytesPromise().await() | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @suppress | ||||||
|  |  */ | ||||||
|  | actual val MPPFile.filename: FileName | ||||||
|  |     get() = FileName(name) | ||||||
|  | /** | ||||||
|  |  * @suppress | ||||||
|  |  */ | ||||||
|  | actual val MPPFile.filesize: Long | ||||||
|  |     get() = size.toLong() | ||||||
|  | /** | ||||||
|  |  * @suppress | ||||||
|  |  */ | ||||||
|  | @Warning("That is not optimized version of bytes allocator. Use asyncBytesAllocator everywhere you can") | ||||||
|  | actual val MPPFile.bytesAllocatorSync: ByteArrayAllocator | ||||||
|  |     get() = ::readBytes | ||||||
|  | /** | ||||||
|  |  * @suppress | ||||||
|  |  */ | ||||||
|  | @Warning("That is not optimized version of bytes allocator. Use asyncBytesAllocator everywhere you can") | ||||||
|  | actual val MPPFile.bytesAllocator: SuspendByteArrayAllocator | ||||||
|  |     get() = ::dirtyReadBytes | ||||||
| @@ -0,0 +1,38 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | import kotlinx.browser.document | ||||||
|  | import org.w3c.dom.* | ||||||
|  | import org.w3c.dom.events.Event | ||||||
|  | import org.w3c.dom.events.EventListener | ||||||
|  |  | ||||||
|  | fun Element.onActionOutside(type: String, options: dynamic = null, callback: (Event) -> Unit): EventListener { | ||||||
|  |     lateinit var observer: MutationObserver | ||||||
|  |     val listener = EventListener { | ||||||
|  |         val elementsToCheck = mutableListOf<Element>(this@onActionOutside) | ||||||
|  |         while (it.target != this@onActionOutside && elementsToCheck.isNotEmpty()) { | ||||||
|  |             val childrenGettingElement = elementsToCheck.removeFirst() | ||||||
|  |             for (i in 0 until childrenGettingElement.childElementCount) { | ||||||
|  |                 elementsToCheck.add(childrenGettingElement.children[i] ?: continue) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         if (elementsToCheck.isEmpty()) { | ||||||
|  |             callback(it) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     if (options == null) { | ||||||
|  |         document.addEventListener(type, listener) | ||||||
|  |     } else { | ||||||
|  |         document.addEventListener(type, listener, options) | ||||||
|  |     } | ||||||
|  |     observer = onRemoved { | ||||||
|  |         if (options == null) { | ||||||
|  |             document.removeEventListener(type, listener) | ||||||
|  |         } else { | ||||||
|  |             document.removeEventListener(type, listener, options) | ||||||
|  |         } | ||||||
|  |         observer.disconnect() | ||||||
|  |     } | ||||||
|  |     return listener | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fun Element.onClickOutside(options: dynamic = null, callback: (Event) -> Unit) = onActionOutside("click", options, callback) | ||||||
| @@ -0,0 +1,8 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | import kotlinx.browser.window | ||||||
|  |  | ||||||
|  | fun openLink(link: String, target: String = "_blank", features: String = "") { | ||||||
|  |     window.open(link, target, features) ?.focus() | ||||||
|  | } | ||||||
|  |  | ||||||
| @@ -0,0 +1,8 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | import kotlin.coroutines.* | ||||||
|  | import kotlin.js.Promise | ||||||
|  |  | ||||||
|  | suspend fun <T> Promise<T>.await(): T = suspendCoroutine { cont -> | ||||||
|  |     then({ cont.resume(it) }, { cont.resumeWithException(it) }) | ||||||
|  | } | ||||||
| @@ -0,0 +1,58 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | import org.w3c.dom.* | ||||||
|  | import kotlin.js.Json | ||||||
|  | import kotlin.js.json | ||||||
|  |  | ||||||
|  | external class ResizeObserver( | ||||||
|  |     callback: (Array<ResizeObserverEntry>, ResizeObserver) -> Unit | ||||||
|  | ) { | ||||||
|  |     fun observe(target: Element, options: Json = definedExternally) | ||||||
|  |  | ||||||
|  |     fun unobserve(target: Element) | ||||||
|  |  | ||||||
|  |     fun disconnect() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | external interface ResizeObserverSize { | ||||||
|  |     val blockSize: Float | ||||||
|  |     val inlineSize: Float | ||||||
|  | } | ||||||
|  |  | ||||||
|  | external interface ResizeObserverEntry { | ||||||
|  |     val borderBoxSize: Array<ResizeObserverSize> | ||||||
|  |     val contentBoxSize: Array<ResizeObserverSize> | ||||||
|  |     val devicePixelContentBoxSize: Array<ResizeObserverSize> | ||||||
|  |     val contentRect: DOMRectReadOnly | ||||||
|  |     val target: Element | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fun ResizeObserver.observe(target: Element, options: ResizeObserverObserveOptions) = observe( | ||||||
|  |     target, | ||||||
|  |     json( | ||||||
|  |         "box" to options.box ?.name | ||||||
|  |     ) | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | class ResizeObserverObserveOptions( | ||||||
|  |     val box: Box? = null | ||||||
|  | ) { | ||||||
|  |     sealed interface Box { | ||||||
|  |         val name: String | ||||||
|  |  | ||||||
|  |         object Content : Box { | ||||||
|  |             override val name: String | ||||||
|  |                 get() = "content-box" | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         object Border : Box { | ||||||
|  |             override val name: String | ||||||
|  |                 get() = "border-box" | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         object DevicePixelContent : Box { | ||||||
|  |             override val name: String | ||||||
|  |                 get() = "device-pixel-content-box" | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,30 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | import kotlinx.browser.document | ||||||
|  | import kotlinx.dom.createElement | ||||||
|  | import org.w3c.dom.HTMLElement | ||||||
|  | import org.w3c.dom.HTMLInputElement | ||||||
|  | import org.w3c.files.get | ||||||
|  |  | ||||||
|  | fun selectFile( | ||||||
|  |     inputSetup: (HTMLInputElement) -> Unit = {}, | ||||||
|  |     onFailure: (Throwable) -> Unit = {}, | ||||||
|  |     onFile: (MPPFile) -> Unit | ||||||
|  | ) { | ||||||
|  |     (document.createElement("input") { | ||||||
|  |         (this as HTMLInputElement).apply { | ||||||
|  |             type = "file" | ||||||
|  |             onchange = { | ||||||
|  |                 runCatching { | ||||||
|  |                     files ?.get(0) ?: error("File must not be null") | ||||||
|  |                 }.onSuccess { | ||||||
|  |                     onFile(it) | ||||||
|  |                 }.onFailure { | ||||||
|  |                     onFailure(it) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             inputSetup(this) | ||||||
|  |         } | ||||||
|  |     } as HTMLElement).click() | ||||||
|  | } | ||||||
|  |  | ||||||
| @@ -0,0 +1,14 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | import kotlinx.browser.document | ||||||
|  | import org.w3c.dom.HTMLAnchorElement | ||||||
|  |  | ||||||
|  | fun triggerDownloadFile(filename: String, fileLink: String) { | ||||||
|  |     val hiddenElement = document.createElement("a") as HTMLAnchorElement | ||||||
|  |  | ||||||
|  |     hiddenElement.href = fileLink | ||||||
|  |     hiddenElement.target = "_blank" | ||||||
|  |     hiddenElement.download = filename | ||||||
|  |     hiddenElement.click() | ||||||
|  | } | ||||||
|  |  | ||||||
| @@ -0,0 +1,4 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | actual fun Float.fixed(signs: Int): Float = this.asDynamic().toFixed(signs.coerceIn(FixedSignsRange)).unsafeCast<String>().toFloat() | ||||||
|  | actual fun Double.fixed(signs: Int): Double = this.asDynamic().toFixed(signs.coerceIn(FixedSignsRange)).unsafeCast<String>().toDouble() | ||||||
| @@ -0,0 +1,20 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | import java.io.File | ||||||
|  | import java.io.InputStream | ||||||
|  | import java.util.UUID | ||||||
|  |  | ||||||
|  | fun InputStream.downloadToTempFile( | ||||||
|  |     fileName: String = UUID.randomUUID().toString(), | ||||||
|  |     fileExtension: String? = ".temp", | ||||||
|  |     folder: File? = null | ||||||
|  | ) = File.createTempFile( | ||||||
|  |     fileName, | ||||||
|  |     fileExtension, | ||||||
|  |     folder | ||||||
|  | ).apply { | ||||||
|  |     outputStream().use { | ||||||
|  |         copyTo(it) | ||||||
|  |     } | ||||||
|  |     deleteOnExit() | ||||||
|  | } | ||||||
| @@ -0,0 +1,37 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | import dev.inmo.micro_utils.coroutines.doInIO | ||||||
|  | import dev.inmo.micro_utils.coroutines.doOutsideOfCoroutine | ||||||
|  | import java.io.File | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @suppress | ||||||
|  |  */ | ||||||
|  | actual typealias MPPFile = File | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @suppress | ||||||
|  |  */ | ||||||
|  | actual val MPPFile.filename: FileName | ||||||
|  |     get() = FileName(name) | ||||||
|  | /** | ||||||
|  |  * @suppress | ||||||
|  |  */ | ||||||
|  | actual val MPPFile.filesize: Long | ||||||
|  |     get() = length() | ||||||
|  | /** | ||||||
|  |  * @suppress | ||||||
|  |  */ | ||||||
|  | actual val MPPFile.bytesAllocatorSync: ByteArrayAllocator | ||||||
|  |     get() = ::readBytes | ||||||
|  | /** | ||||||
|  |  * @suppress | ||||||
|  |  */ | ||||||
|  | actual val MPPFile.bytesAllocator: SuspendByteArrayAllocator | ||||||
|  |     get() = { | ||||||
|  |         doInIO { | ||||||
|  |             doOutsideOfCoroutine { | ||||||
|  |                 readBytes() | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
| @@ -0,0 +1,12 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | import java.math.BigDecimal | ||||||
|  | import java.math.RoundingMode | ||||||
|  |  | ||||||
|  | actual fun Float.fixed(signs: Int): Float = BigDecimal.valueOf(this.toDouble()) | ||||||
|  |     .setScale(signs.coerceIn(FixedSignsRange), RoundingMode.HALF_UP) | ||||||
|  |     .toFloat(); | ||||||
|  |  | ||||||
|  | actual fun Double.fixed(signs: Int): Double = BigDecimal.valueOf(this) | ||||||
|  |     .setScale(signs.coerceIn(FixedSignsRange), RoundingMode.HALF_UP) | ||||||
|  |     .toDouble(); | ||||||
							
								
								
									
										36
									
								
								common/src/linuxX64Main/kotlin/ActualMPPFile.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								common/src/linuxX64Main/kotlin/ActualMPPFile.kt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,36 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | import okio.FileSystem | ||||||
|  | import okio.Path | ||||||
|  | import okio.use | ||||||
|  |  | ||||||
|  | actual typealias MPPFile = Path | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @suppress | ||||||
|  |  */ | ||||||
|  | actual val MPPFile.filename: FileName | ||||||
|  |     get() = FileName(toString()) | ||||||
|  | /** | ||||||
|  |  * @suppress | ||||||
|  |  */ | ||||||
|  | actual val MPPFile.filesize: Long | ||||||
|  |     get() = FileSystem.SYSTEM.openReadOnly(this).use { | ||||||
|  |         it.size() | ||||||
|  |     } | ||||||
|  | /** | ||||||
|  |  * @suppress | ||||||
|  |  */ | ||||||
|  | actual val MPPFile.bytesAllocatorSync: ByteArrayAllocator | ||||||
|  |     get() = { | ||||||
|  |         FileSystem.SYSTEM.read(this) { | ||||||
|  |             readByteArray() | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | /** | ||||||
|  |  * @suppress | ||||||
|  |  */ | ||||||
|  | actual val MPPFile.bytesAllocator: SuspendByteArrayAllocator | ||||||
|  |     get() = { | ||||||
|  |         bytesAllocatorSync() | ||||||
|  |     } | ||||||
							
								
								
									
										26
									
								
								common/src/linuxX64Main/kotlin/fixed.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								common/src/linuxX64Main/kotlin/fixed.kt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | import kotlinx.cinterop.ByteVar | ||||||
|  | import kotlinx.cinterop.allocArray | ||||||
|  | import kotlinx.cinterop.memScoped | ||||||
|  | import kotlinx.cinterop.toKString | ||||||
|  | import platform.posix.snprintf | ||||||
|  | import platform.posix.sprintf | ||||||
|  |  | ||||||
|  | actual fun Float.fixed(signs: Int): Float { | ||||||
|  |     return memScoped { | ||||||
|  |         val buff = allocArray<ByteVar>(Float.SIZE_BYTES * 2) | ||||||
|  |  | ||||||
|  |         sprintf(buff, "%.${signs}f", this@fixed) | ||||||
|  |         buff.toKString().toFloat() | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | actual fun Double.fixed(signs: Int): Double { | ||||||
|  |     return memScoped { | ||||||
|  |         val buff = allocArray<ByteVar>(Double.SIZE_BYTES * 2) | ||||||
|  |  | ||||||
|  |         sprintf(buff, "%.${signs}f", this@fixed) | ||||||
|  |         buff.toKString().toDouble() | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,76 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | import android.os.Bundle | ||||||
|  | import android.os.Parcelable | ||||||
|  | import androidx.fragment.app.Fragment | ||||||
|  | import java.io.Serializable | ||||||
|  | import kotlin.reflect.KProperty | ||||||
|  |  | ||||||
|  | object ArgumentPropertyNullableDelegate { | ||||||
|  |     operator fun <T: Any> getValue(thisRef: Fragment, property: KProperty<*>): T? { | ||||||
|  |         val arguments = thisRef.arguments ?: return null | ||||||
|  |         val key = property.name | ||||||
|  |         return when (property.getter.returnType.classifier) { | ||||||
|  |             // Scalars | ||||||
|  |             String::class -> arguments.getString(key) | ||||||
|  |             Boolean::class -> arguments.getBoolean(key) | ||||||
|  |             Byte::class -> arguments.getByte(key) | ||||||
|  |             Char::class -> arguments.getChar(key) | ||||||
|  |             Double::class -> arguments.getDouble(key) | ||||||
|  |             Float::class -> arguments.getFloat(key) | ||||||
|  |             Int::class -> arguments.getInt(key) | ||||||
|  |             Long::class -> arguments.getLong(key) | ||||||
|  |             Short::class -> arguments.getShort(key) | ||||||
|  |  | ||||||
|  |             // References | ||||||
|  |             Bundle::class -> arguments.getBundle(key) | ||||||
|  |             CharSequence::class -> arguments.getCharSequence(key) | ||||||
|  |             Parcelable::class -> arguments.getParcelable(key) | ||||||
|  |  | ||||||
|  |             // Scalar arrays | ||||||
|  |             BooleanArray::class -> arguments.getBooleanArray(key) | ||||||
|  |             ByteArray::class -> arguments.getByteArray(key) | ||||||
|  |             CharArray::class -> arguments.getCharArray(key) | ||||||
|  |             DoubleArray::class -> arguments.getDoubleArray(key) | ||||||
|  |             FloatArray::class -> arguments.getFloatArray(key) | ||||||
|  |             IntArray::class -> arguments.getIntArray(key) | ||||||
|  |             LongArray::class -> arguments.getLongArray(key) | ||||||
|  |             ShortArray::class -> arguments.getShortArray(key) | ||||||
|  |             Array::class -> { | ||||||
|  |                 val componentType = property.returnType.classifier ?.javaClass ?.componentType!! | ||||||
|  |                 @Suppress("UNCHECKED_CAST") // Checked by reflection. | ||||||
|  |                 when { | ||||||
|  |                     Parcelable::class.java.isAssignableFrom(componentType) -> { | ||||||
|  |                         arguments.getParcelableArray(key) | ||||||
|  |                     } | ||||||
|  |                     String::class.java.isAssignableFrom(componentType) -> { | ||||||
|  |                         arguments.getStringArray(key) | ||||||
|  |                     } | ||||||
|  |                     CharSequence::class.java.isAssignableFrom(componentType) -> { | ||||||
|  |                         arguments.getCharSequenceArray(key) | ||||||
|  |                     } | ||||||
|  |                     Serializable::class.java.isAssignableFrom(componentType) -> { | ||||||
|  |                         arguments.getSerializable(key) | ||||||
|  |                     } | ||||||
|  |                     else -> { | ||||||
|  |                         val valueType = componentType.canonicalName | ||||||
|  |                         throw IllegalArgumentException( | ||||||
|  |                             "Illegal value array type $valueType for key \"$key\"" | ||||||
|  |                         ) | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             Serializable::class -> arguments.getSerializable(key) | ||||||
|  |             else -> null | ||||||
|  |         } as? T | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | object ArgumentPropertyNonNullableDelegate { | ||||||
|  |     operator fun <T: Any> getValue(thisRef: Fragment, property: KProperty<*>): T { | ||||||
|  |         return ArgumentPropertyNullableDelegate.getValue<T>(thisRef, property)!! | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fun argumentOrNull() = ArgumentPropertyNullableDelegate | ||||||
|  | fun argumentOrThrow() = ArgumentPropertyNonNullableDelegate | ||||||
| @@ -0,0 +1,61 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | import android.app.Activity | ||||||
|  | import android.view.View | ||||||
|  | import android.view.ViewGroup | ||||||
|  | import androidx.core.view.children | ||||||
|  | import androidx.fragment.app.Fragment | ||||||
|  |  | ||||||
|  | fun findViewsByTag(viewGroup: ViewGroup, tag: Any?): List<View> { | ||||||
|  |     return viewGroup.children.flatMap { | ||||||
|  |         findViewsByTag(it, tag) | ||||||
|  |     }.toList() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fun findViewsByTag(viewGroup: ViewGroup, key: Int, tag: Any?): List<View> { | ||||||
|  |     return viewGroup.children.flatMap { | ||||||
|  |         findViewsByTag(it, key, tag) | ||||||
|  |     }.toList() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fun findViewsByTag(view: View, tag: Any?): List<View> { | ||||||
|  |     val result = mutableListOf<View>() | ||||||
|  |     if (view.tag == tag) { | ||||||
|  |         result.add(view) | ||||||
|  |     } | ||||||
|  |     if (view is ViewGroup) { | ||||||
|  |         result.addAll(findViewsByTag(view, tag)) | ||||||
|  |     } | ||||||
|  |     return result.toList() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fun findViewsByTag(view: View, key: Int, tag: Any?): List<View> { | ||||||
|  |     val result = mutableListOf<View>() | ||||||
|  |     if (view.getTag(key) == tag) { | ||||||
|  |         result.add(view) | ||||||
|  |     } | ||||||
|  |     if (view is ViewGroup) { | ||||||
|  |         result.addAll(findViewsByTag(view, key, tag)) | ||||||
|  |     } | ||||||
|  |     return result.toList() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fun Activity.findViewsByTag(tag: Any?) = rootView ?.let { | ||||||
|  |     findViewsByTag(it, tag) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fun Activity.findViewsByTag(key: Int, tag: Any?) = rootView ?.let { | ||||||
|  |     findViewsByTag(it, key, tag) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fun Fragment.findViewsByTag(tag: Any?) = view ?.let { | ||||||
|  |     findViewsByTag(it, tag) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fun Fragment.findViewsByTag(key: Int, tag: Any?) = view ?.let { | ||||||
|  |     findViewsByTag(it, key, tag) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fun Fragment.findViewsByTagInActivity(tag: Any?) = activity ?.findViewsByTag(tag) | ||||||
|  |  | ||||||
|  | fun Fragment.findViewsByTagInActivity(key: Int, tag: Any?) = activity ?.findViewsByTag(key, tag) | ||||||
| @@ -0,0 +1,7 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | import android.app.Activity | ||||||
|  | import android.view.View | ||||||
|  |  | ||||||
|  | val Activity.rootView: View? | ||||||
|  |     get() = findViewById<View?>(android.R.id.content) ?.rootView ?: window.decorView.findViewById<View?>(android.R.id.content) ?.rootView | ||||||
| @@ -33,3 +33,15 @@ fun View.toggleVisibility(goneOnHide: Boolean = true) { | |||||||
|         show() |         show() | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | fun View.changeVisibility(show: Boolean = !isShown, goneOnHide: Boolean = true) { | ||||||
|  |     if (show) { | ||||||
|  |         show() | ||||||
|  |     } else { | ||||||
|  |         if (goneOnHide) { | ||||||
|  |             gone() | ||||||
|  |         } else { | ||||||
|  |             hide() | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|   | |||||||
							
								
								
									
										36
									
								
								common/src/mingwX64Main/kotlin/ActualMPPFile.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								common/src/mingwX64Main/kotlin/ActualMPPFile.kt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,36 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | import okio.FileSystem | ||||||
|  | import okio.Path | ||||||
|  | import okio.use | ||||||
|  |  | ||||||
|  | actual typealias MPPFile = Path | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @suppress | ||||||
|  |  */ | ||||||
|  | actual val MPPFile.filename: FileName | ||||||
|  |     get() = FileName(toString()) | ||||||
|  | /** | ||||||
|  |  * @suppress | ||||||
|  |  */ | ||||||
|  | actual val MPPFile.filesize: Long | ||||||
|  |     get() = FileSystem.SYSTEM.openReadOnly(this).use { | ||||||
|  |         it.size() | ||||||
|  |     } | ||||||
|  | /** | ||||||
|  |  * @suppress | ||||||
|  |  */ | ||||||
|  | actual val MPPFile.bytesAllocatorSync: ByteArrayAllocator | ||||||
|  |     get() = { | ||||||
|  |         FileSystem.SYSTEM.read(this) { | ||||||
|  |             readByteArray() | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | /** | ||||||
|  |  * @suppress | ||||||
|  |  */ | ||||||
|  | actual val MPPFile.bytesAllocator: SuspendByteArrayAllocator | ||||||
|  |     get() = { | ||||||
|  |         bytesAllocatorSync() | ||||||
|  |     } | ||||||
							
								
								
									
										26
									
								
								common/src/mingwX64Main/kotlin/fixed.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								common/src/mingwX64Main/kotlin/fixed.kt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | import kotlinx.cinterop.ByteVar | ||||||
|  | import kotlinx.cinterop.allocArray | ||||||
|  | import kotlinx.cinterop.memScoped | ||||||
|  | import kotlinx.cinterop.toKString | ||||||
|  | import platform.posix.snprintf | ||||||
|  | import platform.posix.sprintf | ||||||
|  |  | ||||||
|  | actual fun Float.fixed(signs: Int): Float { | ||||||
|  |     return memScoped { | ||||||
|  |         val buff = allocArray<ByteVar>(Float.SIZE_BYTES * 2) | ||||||
|  |  | ||||||
|  |         sprintf(buff, "%.${signs}f", this@fixed) | ||||||
|  |         buff.toKString().toFloat() | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | actual fun Double.fixed(signs: Int): Double { | ||||||
|  |     return memScoped { | ||||||
|  |         val buff = allocArray<ByteVar>(Double.SIZE_BYTES * 2) | ||||||
|  |  | ||||||
|  |         sprintf(buff, "%.${signs}f", this@fixed) | ||||||
|  |         buff.toKString().toDouble() | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -10,13 +10,19 @@ kotlin { | |||||||
|     sourceSets { |     sourceSets { | ||||||
|         commonMain { |         commonMain { | ||||||
|             dependencies { |             dependencies { | ||||||
|                 api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version" |                 api libs.kt.coroutines | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         jsMain { | ||||||
|  |             dependencies { | ||||||
|  |                 api project(":micro_utils.common") | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         androidMain { |         androidMain { | ||||||
|             dependencies { |             dependencies { | ||||||
|                 api "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version" |                 api libs.kt.coroutines.android | ||||||
|             } |             } | ||||||
|  |             dependsOn(jvmMain) | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
							
								
								
									
										20
									
								
								coroutines/compose/build.gradle
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								coroutines/compose/build.gradle
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | |||||||
|  | plugins { | ||||||
|  |     id "org.jetbrains.kotlin.multiplatform" | ||||||
|  |     id "org.jetbrains.kotlin.plugin.serialization" | ||||||
|  |     id "com.android.library" | ||||||
|  |     alias(libs.plugins.jb.compose) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | apply from: "$mppProjectWithSerializationAndComposePresetPath" | ||||||
|  |  | ||||||
|  | kotlin { | ||||||
|  |     sourceSets { | ||||||
|  |         commonMain { | ||||||
|  |             dependencies { | ||||||
|  |                 api libs.kt.coroutines | ||||||
|  |                 api project(":micro_utils.coroutines") | ||||||
|  |                 api project(":micro_utils.common.compose") | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,60 @@ | |||||||
|  | package dev.inmo.micro_utils.coroutines.compose | ||||||
|  |  | ||||||
|  | import androidx.compose.runtime.* | ||||||
|  | import androidx.compose.runtime.snapshots.SnapshotStateList | ||||||
|  | import dev.inmo.micro_utils.common.applyDiff | ||||||
|  | import dev.inmo.micro_utils.coroutines.ExceptionHandler | ||||||
|  | import dev.inmo.micro_utils.coroutines.defaultSafelyWithoutExceptionHandlerWithNull | ||||||
|  | import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions | ||||||
|  | import kotlinx.coroutines.CoroutineScope | ||||||
|  | import kotlinx.coroutines.Dispatchers | ||||||
|  | import kotlinx.coroutines.flow.Flow | ||||||
|  | import kotlinx.coroutines.flow.StateFlow | ||||||
|  | import kotlinx.coroutines.withContext | ||||||
|  | import kotlin.coroutines.CoroutineContext | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Each value of [this] [Flow] will trigger [applyDiff] to the result [SnapshotStateList] | ||||||
|  |  * | ||||||
|  |  * @param scope Will be used to [subscribeSafelyWithoutExceptions] on [this] to update returned [SnapshotStateList] | ||||||
|  |  * @param useContextOnChange Will be used to change context inside of [subscribeSafelyWithoutExceptions] to ensure that | ||||||
|  |  * change will happen in the required [CoroutineContext]. [Dispatchers.Main] by default | ||||||
|  |  * @param onException Will be passed to the [subscribeSafelyWithoutExceptions] as uncaught exceptions handler | ||||||
|  |  */ | ||||||
|  | @Suppress("NOTHING_TO_INLINE") | ||||||
|  | inline fun <reified T> Flow<List<T>>.asMutableComposeListState( | ||||||
|  |     scope: CoroutineScope, | ||||||
|  |     useContextOnChange: CoroutineContext? = Dispatchers.Main, | ||||||
|  |     noinline onException: ExceptionHandler<List<T>?> = defaultSafelyWithoutExceptionHandlerWithNull, | ||||||
|  | ): SnapshotStateList<T> { | ||||||
|  |     val state = mutableStateListOf<T>() | ||||||
|  |     val changeBlock: suspend (List<T>) -> Unit = useContextOnChange ?.let { | ||||||
|  |         { | ||||||
|  |             withContext(useContextOnChange) { | ||||||
|  |                 state.applyDiff(it) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } ?: { | ||||||
|  |         state.applyDiff(it) | ||||||
|  |     } | ||||||
|  |     subscribeSafelyWithoutExceptions(scope, onException, changeBlock) | ||||||
|  |     return state | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * In fact, it is just classcast of [asMutableComposeListState] to [List] | ||||||
|  |  * | ||||||
|  |  * @param scope Will be used to [subscribeSafelyWithoutExceptions] on [this] to update returned [List] | ||||||
|  |  * @param useContextOnChange Will be used to change context inside of [subscribeSafelyWithoutExceptions] to ensure that | ||||||
|  |  * change will happen in the required [CoroutineContext]. [Dispatchers.Main] by default | ||||||
|  |  * @param onException Will be passed to the [subscribeSafelyWithoutExceptions] as uncaught exceptions handler | ||||||
|  |  * | ||||||
|  |  * @return Changing in time [List] which follow [Flow] values | ||||||
|  |  */ | ||||||
|  | @Suppress("NOTHING_TO_INLINE") | ||||||
|  | inline fun <reified T> Flow<List<T>>.asComposeList( | ||||||
|  |     scope: CoroutineScope, | ||||||
|  |     useContextOnChange: CoroutineContext? = Dispatchers.Main, | ||||||
|  |     noinline onException: ExceptionHandler<List<T>?> = defaultSafelyWithoutExceptionHandlerWithNull, | ||||||
|  | ): List<T> = asMutableComposeListState(scope, useContextOnChange, onException) | ||||||
|  |  | ||||||
| @@ -0,0 +1,94 @@ | |||||||
|  | package dev.inmo.micro_utils.coroutines.compose | ||||||
|  |  | ||||||
|  | import androidx.compose.runtime.* | ||||||
|  | import dev.inmo.micro_utils.common.compose.asState | ||||||
|  | import dev.inmo.micro_utils.coroutines.ExceptionHandler | ||||||
|  | import dev.inmo.micro_utils.coroutines.defaultSafelyWithoutExceptionHandlerWithNull | ||||||
|  | import dev.inmo.micro_utils.coroutines.doInUI | ||||||
|  | import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions | ||||||
|  | import kotlinx.coroutines.CoroutineScope | ||||||
|  | import kotlinx.coroutines.Dispatchers | ||||||
|  | import kotlinx.coroutines.flow.Flow | ||||||
|  | import kotlinx.coroutines.flow.StateFlow | ||||||
|  | import kotlinx.coroutines.withContext | ||||||
|  | import kotlin.coroutines.CoroutineContext | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Will map [this] [Flow] as [MutableState]. Returned [MutableState] WILL NOT change source [Flow] | ||||||
|  |  * | ||||||
|  |  * @param initial First value which will be passed to the result [MutableState] | ||||||
|  |  * @param scope Will be used to [subscribeSafelyWithoutExceptions] on [this] to update returned [MutableState] | ||||||
|  |  * @param useContextOnChange Will be used to change context inside of [subscribeSafelyWithoutExceptions] to ensure that | ||||||
|  |  * change will happen in the required [CoroutineContext]. [Dispatchers.Main] by default | ||||||
|  |  * @param onException Will be passed to the [subscribeSafelyWithoutExceptions] as uncaught exceptions handler | ||||||
|  |  */ | ||||||
|  | fun <T> Flow<T>.asMutableComposeState( | ||||||
|  |     initial: T, | ||||||
|  |     scope: CoroutineScope, | ||||||
|  |     useContextOnChange: CoroutineContext? = Dispatchers.Main, | ||||||
|  |     onException: ExceptionHandler<T?> = defaultSafelyWithoutExceptionHandlerWithNull, | ||||||
|  | ): MutableState<T> { | ||||||
|  |     val state = mutableStateOf(initial) | ||||||
|  |     val changeBlock: suspend (T) -> Unit = useContextOnChange ?.let { | ||||||
|  |         { | ||||||
|  |             withContext(useContextOnChange) { | ||||||
|  |                 state.value = it | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } ?: { | ||||||
|  |         state.value = it | ||||||
|  |     } | ||||||
|  |     subscribeSafelyWithoutExceptions(scope, onException, block = changeBlock) | ||||||
|  |     return state | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Will map [this] [StateFlow] as [MutableState]. Returned [MutableState] WILL NOT change source [StateFlow]. | ||||||
|  |  * This conversation will pass its [StateFlow.value] as the first value | ||||||
|  |  * | ||||||
|  |  * @param scope Will be used to [subscribeSafelyWithoutExceptions] on [this] to update returned [MutableState] | ||||||
|  |  * @param useContextOnChange Will be used to change context inside of [subscribeSafelyWithoutExceptions] to ensure that | ||||||
|  |  * change will happen in the required [CoroutineContext]. [Dispatchers.Main] by default | ||||||
|  |  * @param onException Will be passed to the [subscribeSafelyWithoutExceptions] as uncaught exceptions handler | ||||||
|  |  */ | ||||||
|  | @Suppress("NOTHING_TO_INLINE") | ||||||
|  | inline fun <T> StateFlow<T>.asMutableComposeState( | ||||||
|  |     scope: CoroutineScope, | ||||||
|  |     useContextOnChange: CoroutineContext? = Dispatchers.Main, | ||||||
|  |     noinline onException: ExceptionHandler<T?> = defaultSafelyWithoutExceptionHandlerWithNull, | ||||||
|  | ): MutableState<T> = asMutableComposeState(value, scope, useContextOnChange, onException) | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Will create [MutableState] using [asMutableComposeState] and use [asState] to convert it as immutable state | ||||||
|  |  * | ||||||
|  |  * @param initial First value which will be passed to the result [State] | ||||||
|  |  * @param scope Will be used to [subscribeSafelyWithoutExceptions] on [this] to update returned [State] | ||||||
|  |  * @param useContextOnChange Will be used to change context inside of [subscribeSafelyWithoutExceptions] to ensure that | ||||||
|  |  * change will happen in the required [CoroutineContext]. [Dispatchers.Main] by default | ||||||
|  |  * @param onException Will be passed to the [subscribeSafelyWithoutExceptions] as uncaught exceptions handler | ||||||
|  |  */ | ||||||
|  | fun <T> Flow<T>.asComposeState( | ||||||
|  |     initial: T, | ||||||
|  |     scope: CoroutineScope, | ||||||
|  |     useContextOnChange: CoroutineContext? = Dispatchers.Main, | ||||||
|  |     onException: ExceptionHandler<T?> = defaultSafelyWithoutExceptionHandlerWithNull, | ||||||
|  | ): State<T> { | ||||||
|  |     val state = asMutableComposeState(initial, scope, useContextOnChange, onException) | ||||||
|  |     return state.asState() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Will map [this] [StateFlow] as [State]. This conversation will pass its [StateFlow.value] as the first value | ||||||
|  |  * | ||||||
|  |  * @param scope Will be used to [subscribeSafelyWithoutExceptions] on [this] to update returned [State] | ||||||
|  |  * @param useContextOnChange Will be used to change context inside of [subscribeSafelyWithoutExceptions] to ensure that | ||||||
|  |  * change will happen in the required [CoroutineContext]. [Dispatchers.Main] by default | ||||||
|  |  * @param onException Will be passed to the [subscribeSafelyWithoutExceptions] as uncaught exceptions handler | ||||||
|  |  */ | ||||||
|  | @Suppress("NOTHING_TO_INLINE") | ||||||
|  | inline fun <T> StateFlow<T>.asComposeState( | ||||||
|  |     scope: CoroutineScope, | ||||||
|  |     useContextOnChange: CoroutineContext? = Dispatchers.Main, | ||||||
|  |     noinline onException: ExceptionHandler<T?> = defaultSafelyWithoutExceptionHandlerWithNull, | ||||||
|  | ): State<T> = asComposeState(value, scope, useContextOnChange, onException) | ||||||
|  |  | ||||||
| @@ -0,0 +1,14 @@ | |||||||
|  | package dev.inmo.micro_utils.coroutines.compose | ||||||
|  |  | ||||||
|  | import androidx.compose.runtime.* | ||||||
|  | import kotlinx.coroutines.Job | ||||||
|  | import kotlinx.coroutines.job | ||||||
|  | import kotlin.coroutines.CoroutineContext | ||||||
|  |  | ||||||
|  | fun Composition.linkWithJob(job: Job) { | ||||||
|  |     job.invokeOnCompletion { | ||||||
|  |         this@linkWithJob.dispose() | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fun Composition.linkWithContext(coroutineContext: CoroutineContext) = linkWithJob(coroutineContext.job) | ||||||
| @@ -0,0 +1,26 @@ | |||||||
|  | package dev.inmo.micro_utils.coroutines.compose | ||||||
|  |  | ||||||
|  | import androidx.compose.runtime.* | ||||||
|  | import dev.inmo.micro_utils.common.compose.linkWithElement | ||||||
|  | import kotlinx.coroutines.* | ||||||
|  | import org.jetbrains.compose.web.dom.DOMScope | ||||||
|  | import org.w3c.dom.Element | ||||||
|  |  | ||||||
|  | suspend fun <TElement : Element> renderComposableAndLinkToContext( | ||||||
|  |     root: TElement, | ||||||
|  |     monotonicFrameClock: MonotonicFrameClock = DefaultMonotonicFrameClock, | ||||||
|  |     content: @Composable DOMScope<TElement>.() -> Unit | ||||||
|  | ): Composition = org.jetbrains.compose.web.renderComposable(root, monotonicFrameClock, content).apply { | ||||||
|  |     linkWithContext( | ||||||
|  |         currentCoroutineContext() | ||||||
|  |     ) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | suspend fun <TElement : Element> renderComposableAndLinkToContextAndRoot( | ||||||
|  |     root: TElement, | ||||||
|  |     monotonicFrameClock: MonotonicFrameClock = DefaultMonotonicFrameClock, | ||||||
|  |     content: @Composable DOMScope<TElement>.() -> Unit | ||||||
|  | ): Composition = org.jetbrains.compose.web.renderComposable(root, monotonicFrameClock, content).apply { | ||||||
|  |     linkWithContext(currentCoroutineContext()) | ||||||
|  |     linkWithElement(root) | ||||||
|  | } | ||||||
							
								
								
									
										1
									
								
								coroutines/compose/src/main/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								coroutines/compose/src/main/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | <manifest package="dev.inmo.micro_utils.coroutines.compose"/> | ||||||
| @@ -0,0 +1,100 @@ | |||||||
|  | package dev.inmo.micro_utils.coroutines | ||||||
|  |  | ||||||
|  | import kotlinx.coroutines.CoroutineScope | ||||||
|  | import kotlinx.coroutines.channels.BufferOverflow | ||||||
|  | import kotlinx.coroutines.channels.Channel | ||||||
|  | import kotlinx.coroutines.flow.* | ||||||
|  | import kotlinx.coroutines.sync.Mutex | ||||||
|  | import kotlinx.coroutines.sync.withLock | ||||||
|  | import kotlin.coroutines.cancellation.CancellationException | ||||||
|  |  | ||||||
|  | private sealed interface AccumulatorFlowStep<T> | ||||||
|  | private data class DataRetrievedAccumulatorFlowStep<T>(val data: T) : AccumulatorFlowStep<T> | ||||||
|  | private data class SubscribeAccumulatorFlowStep<T>(val channel: Channel<T>) : AccumulatorFlowStep<T> | ||||||
|  | private data class UnsubscribeAccumulatorFlowStep<T>(val channel: Channel<T>) : AccumulatorFlowStep<T> | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * This [Flow] will have behaviour very similar to [SharedFlow], but there are several differences: | ||||||
|  |  * | ||||||
|  |  * * All unhandled by [FlowCollector] data will not be removed from [AccumulatorFlow] and will be sent to new | ||||||
|  |  * [FlowCollector]s until anybody will handle it | ||||||
|  |  * * Here there are an [activeData] where data [T] will be stored until somebody will handle it | ||||||
|  |  */ | ||||||
|  | class AccumulatorFlow<T>( | ||||||
|  |     sourceDataFlow: Flow<T>, | ||||||
|  |     scope: CoroutineScope | ||||||
|  | ) : AbstractFlow<T>() { | ||||||
|  |     private val subscope = scope.LinkedSupervisorScope() | ||||||
|  |     private val activeData = ArrayDeque<T>() | ||||||
|  |     private val dataMutex = Mutex() | ||||||
|  |     private val channelsForBroadcast = mutableListOf<Channel<T>>() | ||||||
|  |     private val channelsMutex = Mutex() | ||||||
|  |     private val steps = subscope.actor<AccumulatorFlowStep<T>> { step -> | ||||||
|  |         when (step) { | ||||||
|  |             is DataRetrievedAccumulatorFlowStep -> { | ||||||
|  |                 if (activeData.firstOrNull() === step.data) { | ||||||
|  |                     dataMutex.withLock { | ||||||
|  |                         activeData.removeFirst() | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             is SubscribeAccumulatorFlowStep -> channelsMutex.withLock { | ||||||
|  |                 channelsForBroadcast.add(step.channel) | ||||||
|  |                 dataMutex.withLock { | ||||||
|  |                     val dataToSend = activeData.toList() | ||||||
|  |                     safelyWithoutExceptions { | ||||||
|  |                         dataToSend.forEach { step.channel.send(it) } | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             is UnsubscribeAccumulatorFlowStep -> channelsMutex.withLock { | ||||||
|  |                 channelsForBroadcast.remove(step.channel) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     private val subscriptionJob = sourceDataFlow.subscribeSafelyWithoutExceptions(subscope) { | ||||||
|  |         dataMutex.withLock { | ||||||
|  |             activeData.addLast(it) | ||||||
|  |         } | ||||||
|  |         channelsMutex.withLock { | ||||||
|  |             channelsForBroadcast.forEach { channel -> | ||||||
|  |                 safelyWithResult { | ||||||
|  |                     channel.send(it) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override suspend fun collectSafely(collector: FlowCollector<T>) { | ||||||
|  |         val channel = Channel<T>(Channel.UNLIMITED, BufferOverflow.SUSPEND) | ||||||
|  |         steps.send(SubscribeAccumulatorFlowStep(channel)) | ||||||
|  |         val result = runCatchingSafely { | ||||||
|  |             for (data in channel) { | ||||||
|  |                 val emitResult = runCatchingSafely { | ||||||
|  |                     collector.emit(data) | ||||||
|  |                 } | ||||||
|  |                 if (emitResult.isSuccess || emitResult.exceptionOrNull() is CancellationException) { | ||||||
|  |                     steps.send(DataRetrievedAccumulatorFlowStep(data)) | ||||||
|  |                 } | ||||||
|  |                 emitResult.getOrThrow() | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         channel.cancel() | ||||||
|  |         steps.send(UnsubscribeAccumulatorFlowStep(channel)) | ||||||
|  |         result.getOrThrow() | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Creates [AccumulatorFlow] using [this] as base [Flow] | ||||||
|  |  */ | ||||||
|  | fun <T> Flow<T>.accumulatorFlow(scope: CoroutineScope): Flow<T> { | ||||||
|  |     return AccumulatorFlow(this, scope) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Creates [AccumulatorFlow] using [this] with [receiveAsFlow] to get | ||||||
|  |  */ | ||||||
|  | fun <T> Channel<T>.accumulatorFlow(scope: CoroutineScope): Flow<T> { | ||||||
|  |     return receiveAsFlow().accumulatorFlow(scope) | ||||||
|  | } | ||||||
| @@ -0,0 +1,46 @@ | |||||||
|  | package dev.inmo.micro_utils.coroutines | ||||||
|  |  | ||||||
|  | import kotlinx.coroutines.CoroutineScope | ||||||
|  | import kotlinx.coroutines.channels.Channel | ||||||
|  | import kotlin.coroutines.* | ||||||
|  |  | ||||||
|  | interface ActorAction<T> { | ||||||
|  |     suspend operator fun invoke(): T | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Planned to use with [doWithSuspending]. Will execute incoming lambdas sequentially | ||||||
|  |  * | ||||||
|  |  * @see actor | ||||||
|  |  */ | ||||||
|  | fun CoroutineScope.createActionsActor() = actor<suspend () -> Unit> { | ||||||
|  |     it() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Planned to use with [doWithSuspending]. Will execute incoming lambdas sequentially | ||||||
|  |  * | ||||||
|  |  * @see safeActor | ||||||
|  |  */ | ||||||
|  | inline fun CoroutineScope.createSafeActionsActor( | ||||||
|  |     noinline onException: ExceptionHandler<Unit> = defaultSafelyExceptionHandler | ||||||
|  | ) = safeActor<suspend () -> Unit>(Channel.UNLIMITED, onException) { | ||||||
|  |     it() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Must be use with actor created by [createActionsActor] or [createSafeActionsActor]. Will send lambda which will | ||||||
|  |  * execute [action] and return result. | ||||||
|  |  * | ||||||
|  |  * @see suspendCoroutine | ||||||
|  |  * @see safely | ||||||
|  |  */ | ||||||
|  | suspend fun <T> Channel<suspend () -> Unit>.doWithSuspending( | ||||||
|  |     action: ActorAction<T> | ||||||
|  | ) = suspendCoroutine<T> { | ||||||
|  |     trySend { | ||||||
|  |         safely({ e -> it.resumeWithException(e) }) { | ||||||
|  |             it.resume(action()) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -1,19 +1,15 @@ | |||||||
| package dev.inmo.micro_utils.coroutines | package dev.inmo.micro_utils.coroutines | ||||||
|  |  | ||||||
| import kotlinx.coroutines.CoroutineScope | import kotlinx.coroutines.* | ||||||
| import kotlinx.coroutines.channels.Channel | import kotlinx.coroutines.channels.Channel | ||||||
| import kotlinx.coroutines.launch | import kotlinx.coroutines.flow.consumeAsFlow | ||||||
|  |  | ||||||
| fun <T> CoroutineScope.actor( | fun <T> CoroutineScope.actor( | ||||||
|     channelCapacity: Int = Channel.UNLIMITED, |     channelCapacity: Int = Channel.UNLIMITED, | ||||||
|     block: suspend (T) -> Unit |     block: suspend (T) -> Unit | ||||||
| ): Channel<T> { | ): Channel<T> { | ||||||
|     val channel = Channel<T>(channelCapacity) |     val channel = Channel<T>(channelCapacity) | ||||||
|     launch { |     channel.consumeAsFlow().subscribe(this, block) | ||||||
|         for (data in channel) { |  | ||||||
|             block(data) |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|     return channel |     return channel | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -0,0 +1,30 @@ | |||||||
|  | package dev.inmo.micro_utils.coroutines | ||||||
|  |  | ||||||
|  | import kotlinx.coroutines.* | ||||||
|  | import kotlinx.coroutines.channels.Channel | ||||||
|  | import kotlinx.coroutines.flow.consumeAsFlow | ||||||
|  |  | ||||||
|  | fun <T> CoroutineScope.actorAsync( | ||||||
|  |     channelCapacity: Int = Channel.UNLIMITED, | ||||||
|  |     markerFactory: suspend (T) -> Any? = { null }, | ||||||
|  |     block: suspend (T) -> Unit | ||||||
|  | ): Channel<T> { | ||||||
|  |     val channel = Channel<T>(channelCapacity) | ||||||
|  |     channel.consumeAsFlow().subscribeAsync(this, markerFactory, block) | ||||||
|  |     return channel | ||||||
|  | } | ||||||
|  |  | ||||||
|  | inline fun <T> CoroutineScope.safeActorAsync( | ||||||
|  |     channelCapacity: Int = Channel.UNLIMITED, | ||||||
|  |     noinline onException: ExceptionHandler<Unit> = defaultSafelyExceptionHandler, | ||||||
|  |     noinline markerFactory: suspend (T) -> Any? = { null }, | ||||||
|  |     crossinline block: suspend (T) -> Unit | ||||||
|  | ): Channel<T> = actorAsync( | ||||||
|  |     channelCapacity, | ||||||
|  |     markerFactory | ||||||
|  | ) { | ||||||
|  |     safely(onException) { | ||||||
|  |         block(it) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
| @@ -0,0 +1,33 @@ | |||||||
|  | package dev.inmo.micro_utils.coroutines | ||||||
|  |  | ||||||
|  | import kotlinx.coroutines.* | ||||||
|  | import kotlin.coroutines.* | ||||||
|  |  | ||||||
|  | suspend fun <T> Iterable<Deferred<T>>.awaitFirstWithDeferred( | ||||||
|  |     scope: CoroutineScope, | ||||||
|  |     cancelOnResult: Boolean = true | ||||||
|  | ): Pair<Deferred<T>, T> { | ||||||
|  |     val resultDeferred = CompletableDeferred<Pair<Deferred<T>, T>>() | ||||||
|  |     val scope = scope.LinkedSupervisorScope() | ||||||
|  |     forEach { | ||||||
|  |         scope.launch { | ||||||
|  |             resultDeferred.complete(it to it.await()) | ||||||
|  |             scope.cancel() | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     return resultDeferred.await().also { | ||||||
|  |         if (cancelOnResult) { | ||||||
|  |             forEach { | ||||||
|  |                 runCatchingSafely { it.cancel() } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | suspend fun <T> Iterable<Deferred<T>>.awaitFirst( | ||||||
|  |     scope: CoroutineScope, | ||||||
|  |     cancelOnResult: Boolean = true | ||||||
|  | ): T = awaitFirstWithDeferred(scope, cancelOnResult).second | ||||||
|  | suspend fun <T> Iterable<Deferred<T>>.awaitFirst( | ||||||
|  |     cancelOthers: Boolean = true | ||||||
|  | ): T = awaitFirst(CoroutineScope(coroutineContext), cancelOthers) | ||||||
| @@ -0,0 +1,23 @@ | |||||||
|  | package dev.inmo.micro_utils.coroutines | ||||||
|  |  | ||||||
|  | import kotlinx.coroutines.* | ||||||
|  | import kotlin.coroutines.CoroutineContext | ||||||
|  |  | ||||||
|  | inline val UI | ||||||
|  |     get() = Dispatchers.Main | ||||||
|  | inline val Default | ||||||
|  |     get() = Dispatchers.Default | ||||||
|  |  | ||||||
|  | suspend inline fun <T> doIn(context: CoroutineContext, noinline block: suspend CoroutineScope.() -> T) = withContext( | ||||||
|  |     context, | ||||||
|  |     block | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | suspend inline fun <T> doInUI(noinline block: suspend CoroutineScope.() -> T) = doIn( | ||||||
|  |     UI, | ||||||
|  |     block | ||||||
|  | ) | ||||||
|  | suspend inline fun <T> doInDefault(noinline block: suspend CoroutineScope.() -> T) = doIn( | ||||||
|  |     Default, | ||||||
|  |     block | ||||||
|  | ) | ||||||
| @@ -0,0 +1,85 @@ | |||||||
|  | package dev.inmo.micro_utils.coroutines | ||||||
|  |  | ||||||
|  | import kotlinx.coroutines.* | ||||||
|  |  | ||||||
|  | class DeferredAction<T, O>( | ||||||
|  |     val deferred: Deferred<T>, | ||||||
|  |     val callback: suspend (T) -> O | ||||||
|  | ) { | ||||||
|  |     suspend operator fun invoke() = callback(deferred.await()) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class DoWithFirstBuilder<T>( | ||||||
|  |     private val scope: CoroutineScope | ||||||
|  | ) { | ||||||
|  |     private val deferreds = mutableListOf<Deferred<T>>() | ||||||
|  |     operator fun plus(block: suspend CoroutineScope.() -> T) { | ||||||
|  |         deferreds.add(scope.async(start = CoroutineStart.LAZY, block = block)) | ||||||
|  |     } | ||||||
|  |     inline fun add(noinline block: suspend CoroutineScope.() -> T) = plus(block) | ||||||
|  |     inline fun include(noinline block: suspend CoroutineScope.() -> T) = plus(block) | ||||||
|  |  | ||||||
|  |     fun build() = deferreds.toList() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fun <T, O> Deferred<T>.buildAction(callback: suspend (T) -> O) = DeferredAction(this, callback) | ||||||
|  |  | ||||||
|  | suspend fun <O> Iterable<DeferredAction<*, O>>.invokeFirstOf( | ||||||
|  |     scope: CoroutineScope, | ||||||
|  |     cancelOnResult: Boolean = true | ||||||
|  | ): O { | ||||||
|  |     return map { it.deferred }.awaitFirstWithDeferred(scope, cancelOnResult).let { result -> | ||||||
|  |         first { it.deferred == result.first }.invoke() | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | suspend fun <O> invokeFirstOf( | ||||||
|  |     scope: CoroutineScope, | ||||||
|  |     vararg variants: DeferredAction<*, O>, | ||||||
|  |     cancelOnResult: Boolean = true | ||||||
|  | ): O = variants.toList().invokeFirstOf(scope, cancelOnResult) | ||||||
|  |  | ||||||
|  | suspend fun <T, O> Iterable<Deferred<T>>.invokeOnFirst( | ||||||
|  |     scope: CoroutineScope, | ||||||
|  |     cancelOnResult: Boolean = true, | ||||||
|  |     callback: suspend (T) -> O | ||||||
|  | ): O = map { it.buildAction(callback) }.invokeFirstOf(scope, cancelOnResult) | ||||||
|  |  | ||||||
|  | suspend fun <T, O> CoroutineScope.invokeOnFirstOf( | ||||||
|  |     cancelOnResult: Boolean = true, | ||||||
|  |     block: DoWithFirstBuilder<T>.() -> Unit, | ||||||
|  |     callback: suspend (T) -> O | ||||||
|  | ) = firstOf( | ||||||
|  |     DoWithFirstBuilder<T>(this).apply(block).build(), | ||||||
|  |     cancelOnResult | ||||||
|  | ).let { callback(it) } | ||||||
|  |  | ||||||
|  | suspend fun <T, O> invokeOnFirst( | ||||||
|  |     scope: CoroutineScope, | ||||||
|  |     vararg variants: Deferred<T>, | ||||||
|  |     cancelOnResult: Boolean = true, | ||||||
|  |     callback: suspend (T) -> O | ||||||
|  | ): O = variants.toList().invokeOnFirst(scope, cancelOnResult, callback) | ||||||
|  |  | ||||||
|  | suspend fun <T> CoroutineScope.firstOf( | ||||||
|  |     variants: Iterable<Deferred<T>>, | ||||||
|  |     cancelOnResult: Boolean = true | ||||||
|  | ) = variants.invokeOnFirst(this, cancelOnResult) { it } | ||||||
|  |  | ||||||
|  | suspend fun <T> CoroutineScope.firstOf( | ||||||
|  |     cancelOnResult: Boolean = true, | ||||||
|  |     block: DoWithFirstBuilder<T>.() -> Unit | ||||||
|  | ) = firstOf( | ||||||
|  |     DoWithFirstBuilder<T>(this).apply(block).build(), | ||||||
|  |     cancelOnResult | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | suspend fun <T> CoroutineScope.firstOf( | ||||||
|  |     vararg variants: Deferred<T>, | ||||||
|  |     cancelOnResult: Boolean = true | ||||||
|  | ) = firstOf(variants.toList(), cancelOnResult) | ||||||
|  |  | ||||||
|  | suspend fun <T> List<Deferred<T>>.first( | ||||||
|  |     scope: CoroutineScope, | ||||||
|  |     cancelOnResult: Boolean = true | ||||||
|  | ) = scope.firstOf(this, cancelOnResult) | ||||||
| @@ -0,0 +1,6 @@ | |||||||
|  | package dev.inmo.micro_utils.coroutines | ||||||
|  |  | ||||||
|  | import kotlinx.coroutines.flow.Flow | ||||||
|  | import kotlinx.coroutines.flow.first | ||||||
|  |  | ||||||
|  | suspend fun <T> Flow<T?>.firstNotNull() = first { it != null }!! | ||||||
| @@ -0,0 +1,39 @@ | |||||||
|  | package dev.inmo.micro_utils.coroutines | ||||||
|  |  | ||||||
|  | import kotlinx.coroutines.flow.* | ||||||
|  | import kotlin.js.JsName | ||||||
|  | import kotlin.jvm.JvmName | ||||||
|  |  | ||||||
|  | inline fun <T, R> Flow<Flow<T>>.flatMap( | ||||||
|  |     crossinline mapper: suspend (T) -> R | ||||||
|  | ) = flow { | ||||||
|  |     collect { | ||||||
|  |         it.collect { | ||||||
|  |             emit(mapper(it)) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @JsName("flatMapIterable") | ||||||
|  | @JvmName("flatMapIterable") | ||||||
|  | inline fun <T, R> Flow<Iterable<T>>.flatMap( | ||||||
|  |     crossinline mapper: suspend (T) -> R | ||||||
|  | ) = map { | ||||||
|  |     it.asFlow() | ||||||
|  | }.flatMap(mapper) | ||||||
|  |  | ||||||
|  | inline fun <T, R> Flow<Flow<T>>.flatMapNotNull( | ||||||
|  |     crossinline mapper: suspend (T) -> R | ||||||
|  | ) = flatMap(mapper).takeNotNull() | ||||||
|  |  | ||||||
|  | @JsName("flatMapNotNullIterable") | ||||||
|  | @JvmName("flatMapNotNullIterable") | ||||||
|  | inline fun <T, R> Flow<Iterable<T>>.flatMapNotNull( | ||||||
|  |     crossinline mapper: suspend (T) -> R | ||||||
|  | ) = flatMap(mapper).takeNotNull() | ||||||
|  |  | ||||||
|  | fun <T> Flow<Flow<T>>.flatten() = flatMap { it } | ||||||
|  |  | ||||||
|  | @JsName("flattenIterable") | ||||||
|  | @JvmName("flattenIterable") | ||||||
|  | fun <T> Flow<Iterable<T>>.flatten() = flatMap { it } | ||||||
| @@ -0,0 +1,6 @@ | |||||||
|  | package dev.inmo.micro_utils.coroutines | ||||||
|  |  | ||||||
|  | import kotlinx.coroutines.flow.* | ||||||
|  |  | ||||||
|  | fun <T> Flow<T>.takeNotNull() = mapNotNull { it } | ||||||
|  | fun <T> Flow<T>.filterNotNull() = takeNotNull() | ||||||
| @@ -4,6 +4,8 @@ package dev.inmo.micro_utils.coroutines | |||||||
|  |  | ||||||
| import kotlinx.coroutines.CoroutineScope | import kotlinx.coroutines.CoroutineScope | ||||||
| import kotlinx.coroutines.flow.* | import kotlinx.coroutines.flow.* | ||||||
|  | import kotlinx.coroutines.sync.Mutex | ||||||
|  | import kotlinx.coroutines.sync.withLock | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Shortcut for chain if [Flow.onEach] and [Flow.launchIn] |  * Shortcut for chain if [Flow.onEach] and [Flow.launchIn] | ||||||
| @@ -25,13 +27,26 @@ inline fun <T> Flow<T>.subscribeSafely( | |||||||
| } | } | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Use [subscribeSafelyWithoutExceptions], but all exceptions inside of [safely] will be skipped |  * Use [subscribeSafelyWithoutExceptions], but all exceptions will be passed to [defaultSafelyExceptionHandler] | ||||||
|  */ |  */ | ||||||
| inline fun <T> Flow<T>.subscribeSafelyWithoutExceptions( | inline fun <T> Flow<T>.subscribeSafelyWithoutExceptions( | ||||||
|     scope: CoroutineScope, |     scope: CoroutineScope, | ||||||
|  |     noinline onException: ExceptionHandler<T?> = defaultSafelyWithoutExceptionHandlerWithNull, | ||||||
|     noinline block: suspend (T) -> Unit |     noinline block: suspend (T) -> Unit | ||||||
| ) = subscribeSafely( | ) = subscribe(scope) { | ||||||
|     scope, |     safelyWithoutExceptions(onException) { | ||||||
|     {}, |         block(it) | ||||||
|     block |     } | ||||||
| ) | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Use [subscribeSafelyWithoutExceptions], but all exceptions inside of [safely] will be skipped | ||||||
|  |  */ | ||||||
|  | inline fun <T> Flow<T>.subscribeSafelySkippingExceptions( | ||||||
|  |     scope: CoroutineScope, | ||||||
|  |     noinline block: suspend (T) -> Unit | ||||||
|  | ) = subscribe(scope) { | ||||||
|  |     safelyWithoutExceptions({ /* skip exceptions */ }) { | ||||||
|  |         block(it) | ||||||
|  |     } | ||||||
|  | } | ||||||
|   | |||||||
| @@ -0,0 +1,118 @@ | |||||||
|  | package dev.inmo.micro_utils.coroutines | ||||||
|  |  | ||||||
|  | import kotlinx.coroutines.* | ||||||
|  | import kotlinx.coroutines.channels.* | ||||||
|  | import kotlinx.coroutines.flow.* | ||||||
|  | import kotlinx.coroutines.sync.Mutex | ||||||
|  | import kotlinx.coroutines.sync.withLock | ||||||
|  |  | ||||||
|  | private class SubscribeAsyncReceiver<T>( | ||||||
|  |     val scope: CoroutineScope, | ||||||
|  |     output: suspend SubscribeAsyncReceiver<T>.(T) -> Unit | ||||||
|  | ) { | ||||||
|  |     private val dataChannel: Channel<T> = Channel(Channel.UNLIMITED) | ||||||
|  |     val channel: SendChannel<T> | ||||||
|  |         get() = dataChannel | ||||||
|  |  | ||||||
|  |     init { | ||||||
|  |         scope.launchSafelyWithoutExceptions { | ||||||
|  |             for (data in dataChannel) { | ||||||
|  |                 output(data) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fun isEmpty(): Boolean = dataChannel.isEmpty | ||||||
|  | } | ||||||
|  |  | ||||||
|  | private sealed interface AsyncSubscriptionCommand<T, M> { | ||||||
|  |     suspend operator fun invoke(markersMap: MutableMap<M, SubscribeAsyncReceiver<T>>) | ||||||
|  | } | ||||||
|  | private data class AsyncSubscriptionCommandData<T, M>( | ||||||
|  |     val data: T, | ||||||
|  |     val scope: CoroutineScope, | ||||||
|  |     val markerFactory: suspend (T) -> M, | ||||||
|  |     val block: suspend (T) -> Unit, | ||||||
|  |     val onEmpty: suspend (M) -> Unit | ||||||
|  | ) : AsyncSubscriptionCommand<T, M> { | ||||||
|  |     override suspend fun invoke(markersMap: MutableMap<M, SubscribeAsyncReceiver<T>>) { | ||||||
|  |         val marker = markerFactory(data) | ||||||
|  |         markersMap.getOrPut(marker) { | ||||||
|  |             SubscribeAsyncReceiver(scope.LinkedSupervisorScope()) { | ||||||
|  |                 safelyWithoutExceptions { block(it) } | ||||||
|  |                 if (isEmpty()) { | ||||||
|  |                     onEmpty(marker) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         }.channel.send(data) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | private data class AsyncSubscriptionCommandClearReceiver<T, M>( | ||||||
|  |     val marker: M | ||||||
|  | ) : AsyncSubscriptionCommand<T, M> { | ||||||
|  |     override suspend fun invoke(markersMap: MutableMap<M, SubscribeAsyncReceiver<T>>) { | ||||||
|  |         val receiver = markersMap[marker] | ||||||
|  |         if (receiver ?.isEmpty() == true) { | ||||||
|  |             markersMap.remove(marker) | ||||||
|  |             receiver.scope.cancel() | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fun <T, M> Flow<T>.subscribeAsync( | ||||||
|  |     scope: CoroutineScope, | ||||||
|  |     markerFactory: suspend (T) -> M, | ||||||
|  |     block: suspend (T) -> Unit | ||||||
|  | ): Job { | ||||||
|  |     val subscope = scope.LinkedSupervisorScope() | ||||||
|  |     val markersMap = mutableMapOf<M, SubscribeAsyncReceiver<T>>() | ||||||
|  |     val actor = subscope.actor<AsyncSubscriptionCommand<T, M>>(Channel.UNLIMITED) { | ||||||
|  |         it.invoke(markersMap) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     val job = subscribeSafelyWithoutExceptions(subscope) { data -> | ||||||
|  |         val dataCommand = AsyncSubscriptionCommandData(data, subscope, markerFactory, block) { marker -> | ||||||
|  |             actor.send( | ||||||
|  |                 AsyncSubscriptionCommandClearReceiver(marker) | ||||||
|  |             ) | ||||||
|  |         } | ||||||
|  |         actor.send(dataCommand) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     job.invokeOnCompletion { if (subscope.isActive) subscope.cancel() } | ||||||
|  |  | ||||||
|  |     return job | ||||||
|  | } | ||||||
|  |  | ||||||
|  | inline fun <T, M> Flow<T>.subscribeSafelyAsync( | ||||||
|  |     scope: CoroutineScope, | ||||||
|  |     noinline markerFactory: suspend (T) -> M, | ||||||
|  |     noinline onException: ExceptionHandler<Unit> = defaultSafelyExceptionHandler, | ||||||
|  |     noinline block: suspend (T) -> Unit | ||||||
|  | ) = subscribeAsync(scope, markerFactory) { | ||||||
|  |     safely(onException) { | ||||||
|  |         block(it) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | inline fun <T, M> Flow<T>.subscribeSafelyWithoutExceptionsAsync( | ||||||
|  |     scope: CoroutineScope, | ||||||
|  |     noinline markerFactory: suspend (T) -> M, | ||||||
|  |     noinline onException: ExceptionHandler<T?> = defaultSafelyWithoutExceptionHandlerWithNull, | ||||||
|  |     noinline block: suspend (T) -> Unit | ||||||
|  | ) = subscribeAsync(scope, markerFactory) { | ||||||
|  |     safelyWithoutExceptions(onException) { | ||||||
|  |         block(it) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | inline fun <T, M> Flow<T>.subscribeSafelySkippingExceptionsAsync( | ||||||
|  |     scope: CoroutineScope, | ||||||
|  |     noinline markerFactory: suspend (T) -> M, | ||||||
|  |     noinline block: suspend (T) -> Unit | ||||||
|  | ) = subscribeAsync(scope, markerFactory) { | ||||||
|  |     safelyWithoutExceptions({ /* do nothing */}) { | ||||||
|  |         block(it) | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -12,31 +12,14 @@ typealias ExceptionHandler<T> = suspend (Throwable) -> T | |||||||
| var defaultSafelyExceptionHandler: ExceptionHandler<Nothing> = { throw it } | var defaultSafelyExceptionHandler: ExceptionHandler<Nothing> = { throw it } | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Key for [SafelyExceptionHandler] which can be used in [CoroutineContext.get] to get current default |  * This instance will be used in all calls of [safelyWithoutExceptions] as an exception handler for [safely] call | ||||||
|  * [SafelyExceptionHandler] |  | ||||||
|  */ |  */ | ||||||
| @Deprecated("This method will be useless in future major update", ReplaceWith("ContextSafelyExceptionHandlerKey", "dev.inmo.micro_utils.coroutines.ContextSafelyExceptionHandler")) | var defaultSafelyWithoutExceptionHandler: ExceptionHandler<Unit> = { | ||||||
| class SafelyExceptionHandlerKey<T> : CoroutineContext.Key<SafelyExceptionHandler<T>> |     try { | ||||||
|  |         defaultSafelyExceptionHandler(it) | ||||||
| /** |     } catch (e: Throwable) { | ||||||
|  * Shortcut for creating instance of [SafelyExceptionHandlerKey] |          // do nothing | ||||||
|  */ |     } | ||||||
| @Suppress("NOTHING_TO_INLINE") |  | ||||||
| @Deprecated("This method will be useless in future major update", ReplaceWith("ContextSafelyExceptionHandlerKey", "dev.inmo.micro_utils.coroutines.ContextSafelyExceptionHandler")) |  | ||||||
| inline fun <T> safelyExceptionHandlerKey() = SafelyExceptionHandlerKey<T>() |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Wrapper for [ExceptionHandler] which can be used in [CoroutineContext] to set local (for [CoroutineContext]) default |  | ||||||
|  * [ExceptionHandler]. To get it use [CoroutineContext.get] with key [SafelyExceptionHandlerKey] |  | ||||||
|  * |  | ||||||
|  * @see SafelyExceptionHandlerKey |  | ||||||
|  * @see ExceptionHandler |  | ||||||
|  */ |  | ||||||
| @Deprecated("This method will be useless in future major update", ReplaceWith("ContextSafelyExceptionHandler", "dev.inmo.micro_utils.coroutines.ContextSafelyExceptionHandler")) |  | ||||||
| class SafelyExceptionHandler<T>( |  | ||||||
|     val handler: ExceptionHandler<T> |  | ||||||
| ) : CoroutineContext.Element { |  | ||||||
|     override val key: CoroutineContext.Key<*> = safelyExceptionHandlerKey<T>() |  | ||||||
| } | } | ||||||
|  |  | ||||||
| /** | /** | ||||||
| @@ -103,6 +86,9 @@ suspend fun <T> safelyWithContextExceptionHandler( | |||||||
|  * * [CoroutineContext.get] with [SafelyExceptionHandlerKey] as key |  * * [CoroutineContext.get] with [SafelyExceptionHandlerKey] as key | ||||||
|  * * [defaultSafelyExceptionHandler] |  * * [defaultSafelyExceptionHandler] | ||||||
|  * |  * | ||||||
|  |  * Remember, that [ExceptionHandler] from [CoroutineContext.get] will be used anyway if it is available. After it will | ||||||
|  |  * be called [onException] | ||||||
|  |  * | ||||||
|  * @param [onException] Will be called when happen exception inside of [block]. By default will throw exception - this |  * @param [onException] Will be called when happen exception inside of [block]. By default will throw exception - this | ||||||
|  * exception will be available for catching |  * exception will be available for catching | ||||||
|  * |  * | ||||||
| @@ -122,9 +108,60 @@ suspend inline fun <T> safely( | |||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | suspend inline fun <T> runCatchingSafely( | ||||||
|  |     noinline onException: ExceptionHandler<T> = defaultSafelyExceptionHandler, | ||||||
|  |     noinline block: suspend CoroutineScope.() -> T | ||||||
|  | ): Result<T> = runCatching { | ||||||
|  |     safely(onException, block) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | suspend inline fun <T, R> T.runCatchingSafely( | ||||||
|  |     noinline onException: ExceptionHandler<R> = defaultSafelyExceptionHandler, | ||||||
|  |     noinline block: suspend T.() -> R | ||||||
|  | ): Result<R> = runCatching { | ||||||
|  |     safely(onException) { block() } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | suspend inline fun <T> safelyWithResult( | ||||||
|  |     noinline block: suspend CoroutineScope.() -> T | ||||||
|  | ): Result<T> = runCatchingSafely(defaultSafelyExceptionHandler, block) | ||||||
|  |  | ||||||
|  | suspend inline fun <T, R> T.safelyWithResult( | ||||||
|  |     noinline block: suspend T.() -> R | ||||||
|  | ): Result<R> = runCatchingSafely(defaultSafelyExceptionHandler, block) | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Shortcut for [safely] without exception handler (instead of this you will receive null as a result) |  * Use this handler in cases you wish to include handling of exceptions by [defaultSafelyWithoutExceptionHandler] and | ||||||
|  |  * returning null at one time | ||||||
|  |  * | ||||||
|  |  * @see safelyWithoutExceptions | ||||||
|  |  * @see launchSafelyWithoutExceptions | ||||||
|  |  * @see asyncSafelyWithoutExceptions | ||||||
|  |  */ | ||||||
|  | val defaultSafelyWithoutExceptionHandlerWithNull: ExceptionHandler<Nothing?> = { | ||||||
|  |     defaultSafelyWithoutExceptionHandler.invoke(it) | ||||||
|  |     null | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Shortcut for [safely] with exception handler, that as expected must return null in case of impossible creating of | ||||||
|  |  * result from exception (instead of throwing it, by default always returns null) | ||||||
|  */ |  */ | ||||||
| suspend inline fun <T> safelyWithoutExceptions( | suspend inline fun <T> safelyWithoutExceptions( | ||||||
|  |     noinline onException: ExceptionHandler<T?> = defaultSafelyWithoutExceptionHandlerWithNull, | ||||||
|     noinline block: suspend CoroutineScope.() -> T |     noinline block: suspend CoroutineScope.() -> T | ||||||
| ): T? = safely({ null }, block) | ): T? = safely(onException, block) | ||||||
|  |  | ||||||
|  | suspend inline fun <T> runCatchingSafelyWithoutExceptions( | ||||||
|  |     noinline onException: ExceptionHandler<T?> = defaultSafelyWithoutExceptionHandlerWithNull, | ||||||
|  |     noinline block: suspend CoroutineScope.() -> T | ||||||
|  | ): Result<T?> = runCatching { | ||||||
|  |     safelyWithoutExceptions(onException, block) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | inline fun CoroutineScope( | ||||||
|  |     context: CoroutineContext, | ||||||
|  |     noinline defaultExceptionsHandler: ExceptionHandler<Unit> | ||||||
|  | ) = CoroutineScope( | ||||||
|  |     context + ContextSafelyExceptionHandler(defaultExceptionsHandler) | ||||||
|  | ) | ||||||
|   | |||||||
| @@ -0,0 +1,41 @@ | |||||||
|  | package dev.inmo.micro_utils.coroutines | ||||||
|  |  | ||||||
|  | import kotlinx.coroutines.* | ||||||
|  | import kotlin.coroutines.CoroutineContext | ||||||
|  | import kotlin.coroutines.EmptyCoroutineContext | ||||||
|  |  | ||||||
|  | inline fun CoroutineScope.launchSafely( | ||||||
|  |     context: CoroutineContext = EmptyCoroutineContext, | ||||||
|  |     start: CoroutineStart = CoroutineStart.DEFAULT, | ||||||
|  |     noinline onException: ExceptionHandler<Unit> = defaultSafelyExceptionHandler, | ||||||
|  |     noinline block: suspend CoroutineScope.() -> Unit | ||||||
|  | ) = launch(context, start) { | ||||||
|  |     safely(onException, block) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | inline fun CoroutineScope.launchSafelyWithoutExceptions( | ||||||
|  |     context: CoroutineContext = EmptyCoroutineContext, | ||||||
|  |     start: CoroutineStart = CoroutineStart.DEFAULT, | ||||||
|  |     noinline onException: ExceptionHandler<Unit?> = defaultSafelyWithoutExceptionHandlerWithNull, | ||||||
|  |     noinline block: suspend CoroutineScope.() -> Unit | ||||||
|  | ) = launch(context, start) { | ||||||
|  |     safelyWithoutExceptions(onException, block) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | inline fun <T> CoroutineScope.asyncSafely( | ||||||
|  |     context: CoroutineContext = EmptyCoroutineContext, | ||||||
|  |     start: CoroutineStart = CoroutineStart.DEFAULT, | ||||||
|  |     noinline onException: ExceptionHandler<T> = defaultSafelyExceptionHandler, | ||||||
|  |     noinline block: suspend CoroutineScope.() -> T | ||||||
|  | ) = async(context, start) { | ||||||
|  |     safely(onException, block) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | inline fun <T> CoroutineScope.asyncSafelyWithoutExceptions( | ||||||
|  |     context: CoroutineContext = EmptyCoroutineContext, | ||||||
|  |     start: CoroutineStart = CoroutineStart.DEFAULT, | ||||||
|  |     noinline onException: ExceptionHandler<T?> = defaultSafelyWithoutExceptionHandlerWithNull, | ||||||
|  |     noinline block: suspend CoroutineScope.() -> T | ||||||
|  | ) = async(context, start) { | ||||||
|  |     safelyWithoutExceptions(onException, block) | ||||||
|  | } | ||||||
| @@ -0,0 +1,136 @@ | |||||||
|  | package dev.inmo.micro_utils.coroutines | ||||||
|  |  | ||||||
|  | import kotlinx.coroutines.currentCoroutineContext | ||||||
|  | import kotlinx.coroutines.flow.MutableStateFlow | ||||||
|  | import kotlinx.coroutines.flow.StateFlow | ||||||
|  | import kotlinx.coroutines.flow.asStateFlow | ||||||
|  | import kotlinx.coroutines.flow.first | ||||||
|  | import kotlinx.coroutines.isActive | ||||||
|  | import kotlinx.coroutines.sync.Mutex | ||||||
|  | import kotlinx.coroutines.sync.withLock | ||||||
|  | import kotlin.contracts.ExperimentalContracts | ||||||
|  | import kotlin.contracts.InvocationKind | ||||||
|  | import kotlin.contracts.contract | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * It is interface which will work like classic [Mutex], but in difference have [lockStateFlow] for listening of the | ||||||
|  |  * [SmartMutex] state. | ||||||
|  |  * | ||||||
|  |  * There is [Mutable] and [Immutable] realizations. In case you are owner and manager current state of lock, you need | ||||||
|  |  * [Mutable] [SmartMutex]. Otherwise, [Immutable]. | ||||||
|  |  * | ||||||
|  |  * Any [Mutable] [SmartMutex] may produce its [Immutable] variant which will contains [lockStateFlow] equal to its | ||||||
|  |  * [Mutable] creator | ||||||
|  |  */ | ||||||
|  | sealed interface SmartMutex { | ||||||
|  |     val lockStateFlow: StateFlow<Boolean> | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * * True - locked | ||||||
|  |      * * False - unlocked | ||||||
|  |      */ | ||||||
|  |     val isLocked: Boolean | ||||||
|  |         get() = lockStateFlow.value | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Immutable variant of [SmartMutex]. In fact will depend on the owner of [lockStateFlow] | ||||||
|  |      */ | ||||||
|  |     class Immutable(override val lockStateFlow: StateFlow<Boolean>) : SmartMutex | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Mutable variant of [SmartMutex]. With that variant you may [lock] and [unlock]. Besides, you may create | ||||||
|  |      * [Immutable] variant of [this] instance with [immutable] factory | ||||||
|  |      * | ||||||
|  |      * @param locked Preset state of [isLocked] and its internal [_lockStateFlow] | ||||||
|  |      */ | ||||||
|  |     class Mutable(locked: Boolean = false) : SmartMutex { | ||||||
|  |         private val _lockStateFlow = MutableStateFlow<Boolean>(locked) | ||||||
|  |         override val lockStateFlow: StateFlow<Boolean> = _lockStateFlow.asStateFlow() | ||||||
|  |  | ||||||
|  |         private val internalChangesMutex = Mutex() | ||||||
|  |  | ||||||
|  |         fun immutable() = Immutable(lockStateFlow) | ||||||
|  |  | ||||||
|  |         /** | ||||||
|  |          * Holds call until this [SmartMutex] will be re-locked. That means that while [isLocked] == true, [holds] will | ||||||
|  |          * wait for [isLocked] == false and then try to lock | ||||||
|  |          */ | ||||||
|  |         suspend fun lock() { | ||||||
|  |             do { | ||||||
|  |                 waitUnlock() | ||||||
|  |                 val shouldContinue = internalChangesMutex.withLock { | ||||||
|  |                     if (_lockStateFlow.value) { | ||||||
|  |                         true | ||||||
|  |                     } else { | ||||||
|  |                         _lockStateFlow.value = true | ||||||
|  |                         false | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } while (shouldContinue && currentCoroutineContext().isActive) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         /** | ||||||
|  |          * Will try to lock this [SmartMutex] immediataly | ||||||
|  |          * | ||||||
|  |          * @return True if lock was successful. False otherwise | ||||||
|  |          */ | ||||||
|  |         suspend fun tryLock(): Boolean { | ||||||
|  |             return if (!_lockStateFlow.value) { | ||||||
|  |                 internalChangesMutex.withLock { | ||||||
|  |                     if (!_lockStateFlow.value) { | ||||||
|  |                         _lockStateFlow.value = true | ||||||
|  |                         true | ||||||
|  |                     } else { | ||||||
|  |                         false | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } else { | ||||||
|  |                 false | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         /** | ||||||
|  |          * If [isLocked] == true - will change it to false and return true. If current call will not unlock this | ||||||
|  |          * [SmartMutex] - false | ||||||
|  |          */ | ||||||
|  |         suspend fun unlock(): Boolean { | ||||||
|  |             return if (_lockStateFlow.value) { | ||||||
|  |                 internalChangesMutex.withLock { | ||||||
|  |                     if (_lockStateFlow.value) { | ||||||
|  |                         _lockStateFlow.value = false | ||||||
|  |                         true | ||||||
|  |                     } else { | ||||||
|  |                         false | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } else { | ||||||
|  |                 false | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Will call [SmartMutex.Mutable.lock], then execute [action] and return the result after [SmartMutex.Mutable.unlock] | ||||||
|  |  */ | ||||||
|  | @OptIn(ExperimentalContracts::class) | ||||||
|  | suspend inline fun <T> SmartMutex.Mutable.withLock(action: () -> T): T { | ||||||
|  |     contract { | ||||||
|  |         callsInPlace(action, InvocationKind.EXACTLY_ONCE) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     lock() | ||||||
|  |     try { | ||||||
|  |         return action() | ||||||
|  |     } finally { | ||||||
|  |         unlock() | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Will wait until the [SmartMutex.lockStateFlow] of [this] instance will be false. | ||||||
|  |  * | ||||||
|  |  * Anyway, after the end of this block there are no any guaranties that [SmartMutex.isLocked] == false due to the fact | ||||||
|  |  * that some other parties may lock it again | ||||||
|  |  */ | ||||||
|  | suspend fun SmartMutex.waitUnlock() = lockStateFlow.first { !it } | ||||||
| @@ -0,0 +1,17 @@ | |||||||
|  | package dev.inmo.micro_utils.coroutines | ||||||
|  |  | ||||||
|  | import kotlinx.coroutines.* | ||||||
|  | import kotlin.coroutines.CoroutineContext | ||||||
|  |  | ||||||
|  | fun CoroutineContext.LinkedSupervisorJob( | ||||||
|  |     additionalContext: CoroutineContext? = null | ||||||
|  | ) = SupervisorJob(job).let { if (additionalContext != null) it + additionalContext else it } | ||||||
|  | fun CoroutineScope.LinkedSupervisorJob( | ||||||
|  |     additionalContext: CoroutineContext? = null | ||||||
|  | ) = coroutineContext.LinkedSupervisorJob(additionalContext) | ||||||
|  |  | ||||||
|  | fun CoroutineScope.LinkedSupervisorScope( | ||||||
|  |     additionalContext: CoroutineContext? = null | ||||||
|  | ) = CoroutineScope( | ||||||
|  |     coroutineContext + LinkedSupervisorJob(additionalContext) | ||||||
|  | ) | ||||||
| @@ -0,0 +1,31 @@ | |||||||
|  | package dev.inmo.micro_utils.coroutines | ||||||
|  |  | ||||||
|  | import kotlinx.coroutines.* | ||||||
|  | import kotlin.coroutines.CoroutineContext | ||||||
|  | import kotlin.coroutines.EmptyCoroutineContext | ||||||
|  |  | ||||||
|  | private fun CoroutineScope.createWeakSubScope() = CoroutineScope(coroutineContext.minusKey(Job)).also { newScope -> | ||||||
|  |     coroutineContext.job.invokeOnCompletion { newScope.cancel() } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fun CoroutineScope.weakLaunch( | ||||||
|  |     context: CoroutineContext = EmptyCoroutineContext, | ||||||
|  |     start: CoroutineStart = CoroutineStart.DEFAULT, | ||||||
|  |     block: suspend CoroutineScope.() -> Unit | ||||||
|  | ): Job { | ||||||
|  |     val scope = createWeakSubScope() | ||||||
|  |     val job = scope.launch(context, start, block) | ||||||
|  |     job.invokeOnCompletion { scope.cancel() } | ||||||
|  |     return job | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fun <T> CoroutineScope.weakAsync( | ||||||
|  |     context: CoroutineContext = EmptyCoroutineContext, | ||||||
|  |     start: CoroutineStart = CoroutineStart.DEFAULT, | ||||||
|  |     block: suspend CoroutineScope.() -> T | ||||||
|  | ): Deferred<T> { | ||||||
|  |     val scope = createWeakSubScope() | ||||||
|  |     val deferred = scope.async(context, start, block) | ||||||
|  |     deferred.invokeOnCompletion { scope.cancel() } | ||||||
|  |     return deferred | ||||||
|  | } | ||||||
| @@ -0,0 +1,10 @@ | |||||||
|  | package dev.inmo.micro_utils.coroutines | ||||||
|  |  | ||||||
|  | import kotlinx.coroutines.await | ||||||
|  | import org.khronos.webgl.Int8Array | ||||||
|  | import org.w3c.fetch.Response | ||||||
|  | import org.w3c.files.Blob | ||||||
|  |  | ||||||
|  | suspend fun Blob.toByteArray() = Int8Array( | ||||||
|  |     Response(this).arrayBuffer().await() | ||||||
|  | ) as ByteArray | ||||||
| @@ -0,0 +1,28 @@ | |||||||
|  | package dev.inmo.micro_utils.coroutines | ||||||
|  |  | ||||||
|  | import dev.inmo.micro_utils.common.onRemoved | ||||||
|  | import dev.inmo.micro_utils.common.onVisibilityChanged | ||||||
|  | import kotlinx.coroutines.flow.* | ||||||
|  | import org.w3c.dom.Element | ||||||
|  |  | ||||||
|  | fun Element.visibilityFlow(): Flow<Boolean> = channelFlow { | ||||||
|  |     var previousData: Boolean? = null | ||||||
|  |  | ||||||
|  |     val observer = onVisibilityChanged { intersectionRatio, _ -> | ||||||
|  |         val currentData = intersectionRatio > 0 | ||||||
|  |         if (currentData != previousData) { | ||||||
|  |             trySend(currentData) | ||||||
|  |         } | ||||||
|  |         previousData = currentData | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     val removeObserver = onRemoved { | ||||||
|  |         observer.disconnect() | ||||||
|  |         close() | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     invokeOnClose { | ||||||
|  |         observer.disconnect() | ||||||
|  |         removeObserver.disconnect() | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,26 @@ | |||||||
|  | package dev.inmo.micro_utils.coroutines | ||||||
|  |  | ||||||
|  | import kotlinx.coroutines.CancellationException | ||||||
|  | import kotlinx.coroutines.Job | ||||||
|  | import org.w3c.dom.Image | ||||||
|  |  | ||||||
|  | suspend fun preloadImage(src: String): Image { | ||||||
|  |     val image = Image() | ||||||
|  |     image.src = src | ||||||
|  |  | ||||||
|  |     val job = Job() | ||||||
|  |  | ||||||
|  |     image.addEventListener("load", { | ||||||
|  |         runCatching { job.complete() } | ||||||
|  |     }) | ||||||
|  |  | ||||||
|  |     runCatchingSafely { | ||||||
|  |         job.join() | ||||||
|  |     }.onFailure { | ||||||
|  |         if (it is CancellationException) { | ||||||
|  |             image.src = "" | ||||||
|  |         } | ||||||
|  |     }.getOrThrow() | ||||||
|  |  | ||||||
|  |     return image | ||||||
|  | } | ||||||
| @@ -0,0 +1,42 @@ | |||||||
|  | package dev.inmo.micro_utils.coroutines | ||||||
|  |  | ||||||
|  | import dev.inmo.micro_utils.common.MPPFile | ||||||
|  | import dev.inmo.micro_utils.common.selectFile | ||||||
|  | import kotlinx.coroutines.CompletableDeferred | ||||||
|  | import org.w3c.dom.HTMLInputElement | ||||||
|  |  | ||||||
|  | suspend fun selectFileOrThrow( | ||||||
|  |     inputSetup: (HTMLInputElement) -> Unit = {} | ||||||
|  | ): MPPFile { | ||||||
|  |     val result = CompletableDeferred<MPPFile>() | ||||||
|  |  | ||||||
|  |     selectFile( | ||||||
|  |         inputSetup, | ||||||
|  |         { | ||||||
|  |             result.completeExceptionally(it) | ||||||
|  |         } | ||||||
|  |     ) { | ||||||
|  |         result.complete(it) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return result.await() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | suspend fun selectFileOrNull( | ||||||
|  |     inputSetup: (HTMLInputElement) -> Unit = {}, | ||||||
|  |     onFailure: (Throwable) -> Unit = {} | ||||||
|  | ): MPPFile? { | ||||||
|  |     val result = CompletableDeferred<MPPFile?>() | ||||||
|  |  | ||||||
|  |     selectFile( | ||||||
|  |         inputSetup, | ||||||
|  |         { | ||||||
|  |             result.complete(null) | ||||||
|  |             onFailure(it) | ||||||
|  |         } | ||||||
|  |     ) { | ||||||
|  |         result.complete(it) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return result.await() | ||||||
|  | } | ||||||
| @@ -0,0 +1,11 @@ | |||||||
|  | package dev.inmo.micro_utils.coroutines | ||||||
|  |  | ||||||
|  | import kotlinx.coroutines.* | ||||||
|  |  | ||||||
|  | val IO | ||||||
|  |     get() = Dispatchers.IO | ||||||
|  |  | ||||||
|  | suspend inline fun <T> doInIO(noinline block: suspend CoroutineScope.() -> T) = doIn( | ||||||
|  |     IO, | ||||||
|  |     block | ||||||
|  | ) | ||||||
| @@ -0,0 +1,9 @@ | |||||||
|  | package dev.inmo.micro_utils.coroutines | ||||||
|  |  | ||||||
|  | import kotlinx.coroutines.CoroutineScope | ||||||
|  | import kotlinx.coroutines.Dispatchers | ||||||
|  |  | ||||||
|  | fun <T> launchInCurrentThread(block: suspend CoroutineScope.() -> T): T { | ||||||
|  |     val scope = CoroutineScope(Dispatchers.Unconfined) | ||||||
|  |     return scope.launchSynchronously(block) | ||||||
|  | } | ||||||
| @@ -3,28 +3,24 @@ package dev.inmo.micro_utils.coroutines | |||||||
| import kotlinx.coroutines.* | import kotlinx.coroutines.* | ||||||
|  |  | ||||||
| fun <T> CoroutineScope.launchSynchronously(block: suspend CoroutineScope.() -> T): T { | fun <T> CoroutineScope.launchSynchronously(block: suspend CoroutineScope.() -> T): T { | ||||||
|     var throwable: Throwable? = null |     var result: Result<T>? = null | ||||||
|     var result: T? = null |     val objectToSynchronize = Object() | ||||||
|     val objectToSynchronize = java.lang.Object() |     synchronized(objectToSynchronize) { | ||||||
|     val launchCallback = { |         launch(start = CoroutineStart.UNDISPATCHED) { | ||||||
|         launch { |             result = safelyWithResult(block) | ||||||
|             safely( |         }.invokeOnCompletion { | ||||||
|                 { |  | ||||||
|                     throwable = it |  | ||||||
|                 } |  | ||||||
|             ) { |  | ||||||
|                 result = block() |  | ||||||
|             } |  | ||||||
|             synchronized(objectToSynchronize) { |             synchronized(objectToSynchronize) { | ||||||
|                 objectToSynchronize.notifyAll() |                 objectToSynchronize.notifyAll() | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |         while (result == null) { | ||||||
|  |             objectToSynchronize.wait() | ||||||
|  |         } | ||||||
|     } |     } | ||||||
|     synchronized(objectToSynchronize) { |     return result!!.getOrThrow() | ||||||
|         launchCallback() |  | ||||||
|         objectToSynchronize.wait() |  | ||||||
|     } |  | ||||||
|     throw throwable ?: return result!! |  | ||||||
| } | } | ||||||
|  |  | ||||||
| fun <T> launchSynchronously(block: suspend CoroutineScope.() -> T): T = CoroutineScope(Dispatchers.Default).launchSynchronously(block) | fun <T> launchSynchronously(block: suspend CoroutineScope.() -> T): T = CoroutineScope(Dispatchers.Default).launchSynchronously(block) | ||||||
|  |  | ||||||
|  | fun <T> CoroutineScope.doSynchronously(block: suspend CoroutineScope.() -> T): T = launchSynchronously(block) | ||||||
|  | fun <T> doSynchronously(block: suspend CoroutineScope.() -> T): T = launchSynchronously(block) | ||||||
|   | |||||||
| @@ -0,0 +1,34 @@ | |||||||
|  | package dev.inmo.micro_utils.coroutines | ||||||
|  |  | ||||||
|  | import kotlinx.coroutines.* | ||||||
|  | import org.junit.Test | ||||||
|  | import kotlin.test.assertEquals | ||||||
|  | import kotlin.test.assertTrue | ||||||
|  |  | ||||||
|  | class AwaitFirstTests { | ||||||
|  |     private fun CoroutineScope.createTestDeferred(value: Int, wait: Long = 100000) = async(start = CoroutineStart.LAZY) { delay(wait); value } | ||||||
|  |     @Test | ||||||
|  |     fun testThatAwaitFirstIsWorkingCorrectly() { | ||||||
|  |         val baseScope = CoroutineScope(Dispatchers.Default) | ||||||
|  |         val resultDeferred = baseScope.createTestDeferred(-1, 0) | ||||||
|  |         val deferreds = listOf( | ||||||
|  |             baseScope.async { createTestDeferred(0) }, | ||||||
|  |             baseScope.async { createTestDeferred(1) }, | ||||||
|  |             baseScope.async { createTestDeferred(2) }, | ||||||
|  |             resultDeferred | ||||||
|  |         ) | ||||||
|  |         val controlJob = baseScope.launch { | ||||||
|  |             delay(1000000) | ||||||
|  |         } | ||||||
|  |         val result = baseScope.launchSynchronously { | ||||||
|  |             val result = deferreds.awaitFirst(baseScope) | ||||||
|  |  | ||||||
|  |             assertTrue(baseScope.isActive) | ||||||
|  |             assertTrue(controlJob.isActive) | ||||||
|  |  | ||||||
|  |             result | ||||||
|  |         } | ||||||
|  |         assertEquals(baseScope.launchSynchronously { resultDeferred.await() }, result) | ||||||
|  |         assertTrue(deferreds.all { it == resultDeferred || it.isCancelled }) | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,26 @@ | |||||||
|  | package dev.inmo.micro_utils.coroutines | ||||||
|  |  | ||||||
|  | import kotlinx.coroutines.* | ||||||
|  | import kotlin.test.Test | ||||||
|  | import kotlin.test.assertEquals | ||||||
|  |  | ||||||
|  | class DoWithFirstTests { | ||||||
|  |     @Test | ||||||
|  |     fun testHandleOneOf() { | ||||||
|  |         val scope = CoroutineScope(Dispatchers.Default) | ||||||
|  |         val happenedDeferreds = mutableListOf<Int>() | ||||||
|  |         val deferredWhichMustHappen = (-1).asDeferred | ||||||
|  |         scope.launchSynchronously { | ||||||
|  |             scope.launch { | ||||||
|  |                 ((0 until 100).map { | ||||||
|  |                     DeferredAction( | ||||||
|  |                         scope.async { delay(10000); it }, | ||||||
|  |                         happenedDeferreds::add | ||||||
|  |                     ) | ||||||
|  |                 } + DeferredAction(deferredWhichMustHappen, happenedDeferreds::add)).invokeFirstOf(scope) | ||||||
|  |             }.join() | ||||||
|  |         } | ||||||
|  |         assertEquals(1, happenedDeferreds.size) | ||||||
|  |         assertEquals(scope.launchSynchronously { deferredWhichMustHappen.await() }, happenedDeferreds.first()) | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,47 @@ | |||||||
|  | package dev.inmo.micro_utils.coroutines | ||||||
|  |  | ||||||
|  | import kotlinx.coroutines.Dispatchers | ||||||
|  | import kotlinx.coroutines.delay | ||||||
|  | import kotlinx.coroutines.withContext | ||||||
|  | import kotlin.test.Test | ||||||
|  | import kotlin.test.assertEquals | ||||||
|  |  | ||||||
|  | class LaunchInCurrentThreadTests { | ||||||
|  |     @Test | ||||||
|  |     fun simpleTestThatLaunchInCurrentThreadWorks() { | ||||||
|  |         val expectedResult = 10 | ||||||
|  |         val result = launchInCurrentThread { | ||||||
|  |             expectedResult | ||||||
|  |         } | ||||||
|  |         assertEquals(expectedResult, result) | ||||||
|  |     } | ||||||
|  |     @Test | ||||||
|  |     fun simpleTestThatSeveralLaunchInCurrentThreadWorks() { | ||||||
|  |         val testData = 0 until 100 | ||||||
|  |  | ||||||
|  |         testData.forEach { | ||||||
|  |             val result = launchInCurrentThread { | ||||||
|  |                 it | ||||||
|  |             } | ||||||
|  |             assertEquals(it, result) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     @Test | ||||||
|  |     fun simpleTestThatLaunchInCurrentThreadWillCorrectlyHandleSuspensionsWorks() { | ||||||
|  |         val testData = 0 until 100 | ||||||
|  |  | ||||||
|  |         suspend fun test(data: Any): Any { | ||||||
|  |             return withContext(Dispatchers.Default) { | ||||||
|  |                 delay(1) | ||||||
|  |                 data | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         testData.forEach { | ||||||
|  |             val result = launchInCurrentThread { | ||||||
|  |                 test(it) | ||||||
|  |             } | ||||||
|  |             assertEquals(it, result) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,40 @@ | |||||||
|  | package dev.inmo.micro_utils.coroutines | ||||||
|  |  | ||||||
|  | import kotlinx.coroutines.* | ||||||
|  | import org.junit.Test | ||||||
|  |  | ||||||
|  | class WeakJob { | ||||||
|  |     @Test | ||||||
|  |     fun `test that weak jobs works correctly`() { | ||||||
|  |         val scope = CoroutineScope(Dispatchers.Default) | ||||||
|  |         lateinit var weakLaunchJob: Job | ||||||
|  |         lateinit var weakAsyncJob: Job | ||||||
|  |         scope.launchSynchronously { | ||||||
|  |             val completeDeferred = Job() | ||||||
|  |             coroutineScope { | ||||||
|  |                 weakLaunchJob = weakLaunch { | ||||||
|  |                     while (isActive) { | ||||||
|  |                         delay(100L) | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |                 weakAsyncJob = weakAsync { | ||||||
|  |                     while (isActive) { | ||||||
|  |                         delay(100L) | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 coroutineContext.job.invokeOnCompletion { | ||||||
|  |                     scope.launch { | ||||||
|  |                         delay(1000L) | ||||||
|  |                         completeDeferred.complete() | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |                 launch { delay(1000L); cancel() } | ||||||
|  |             } | ||||||
|  |             completeDeferred.join() | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         assert(!weakLaunchJob.isActive) | ||||||
|  |         assert(!weakAsyncJob.isActive) | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										50
									
								
								coroutines/src/main/kotlin/FlowOnHierarchyChangeListener.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								coroutines/src/main/kotlin/FlowOnHierarchyChangeListener.kt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,50 @@ | |||||||
|  | package dev.inmo.micro_utils.coroutines | ||||||
|  |  | ||||||
|  | import android.view.View | ||||||
|  | import android.view.ViewGroup | ||||||
|  | import kotlinx.coroutines.flow.MutableSharedFlow | ||||||
|  | import kotlinx.coroutines.flow.asSharedFlow | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * [kotlinx.coroutines.flow.Flow]-based [android.view.ViewGroup.OnHierarchyChangeListener] | ||||||
|  |  * | ||||||
|  |  * @param recursive If set, any call of [onChildViewAdded] will check if child [View] is [ViewGroup] and subscribe to this | ||||||
|  |  * [ViewGroup] too | ||||||
|  |  * @param [_onChildViewAdded] Internal [MutableSharedFlow] which will be used to pass data to [onChildViewAdded] flow | ||||||
|  |  * @param [_onChildViewRemoved] Internal [MutableSharedFlow] which will be used to pass data to [onChildViewRemoved] flow | ||||||
|  |  */ | ||||||
|  | class FlowOnHierarchyChangeListener( | ||||||
|  |     private val recursive: Boolean = false, | ||||||
|  |     private val _onChildViewAdded: MutableSharedFlow<Pair<View, View>> = MutableSharedFlow(extraBufferCapacity = Int.MAX_VALUE), | ||||||
|  |     private val _onChildViewRemoved: MutableSharedFlow<Pair<View, View>> = MutableSharedFlow(extraBufferCapacity = Int.MAX_VALUE) | ||||||
|  | ) : ViewGroup.OnHierarchyChangeListener { | ||||||
|  |     val onChildViewAdded = _onChildViewAdded.asSharedFlow() | ||||||
|  |     val onChildViewRemoved = _onChildViewRemoved.asSharedFlow() | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Will emit data into [onChildViewAdded] flow. If [recursive] is true and [child] is [ViewGroup] will also | ||||||
|  |      * subscribe to [child] hierarchy changes. | ||||||
|  |      * | ||||||
|  |      * Due to the fact that this method is not suspendable, [FlowOnHierarchyChangeListener] will use | ||||||
|  |      * [MutableSharedFlow.tryEmit] to send data into [_onChildViewAdded]. That is why its default extraBufferCapacity is | ||||||
|  |      * [Int.MAX_VALUE] | ||||||
|  |      */ | ||||||
|  |     override fun onChildViewAdded(parent: View, child: View) { | ||||||
|  |         _onChildViewAdded.tryEmit(parent to child) | ||||||
|  |  | ||||||
|  |         if (recursive && child is ViewGroup) { | ||||||
|  |             child.setOnHierarchyChangeListener(this) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Just emit data into [onChildViewRemoved] | ||||||
|  |      * | ||||||
|  |      * Due to the fact that this method is not suspendable, [FlowOnHierarchyChangeListener] will use | ||||||
|  |      * [MutableSharedFlow.tryEmit] to send data into [_onChildViewRemoved]. That is why its default extraBufferCapacity is | ||||||
|  |      * [Int.MAX_VALUE] | ||||||
|  |      */ | ||||||
|  |     override fun onChildViewRemoved(parent: View, child: View) { | ||||||
|  |         _onChildViewRemoved.tryEmit(parent to child) | ||||||
|  |     } | ||||||
|  | } | ||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user