mirror of
				https://github.com/InsanusMokrassar/MicroUtils.git
				synced 2025-10-26 17:50:41 +00:00 
			
		
		
		
	Compare commits
	
		
			963 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | |||
| 6a89ffaa8a | |||
| d5a8d0f4d4 | |||
| 9739bd871e | |||
| 632d2545d4 | |||
| ecfa273a81 | |||
| a36828116e | |||
| 14aa9ca26c | |||
| 069e51f2ff | |||
| a15cbdfb1a | |||
| 4af8114eda | |||
| 90dc84e900 | |||
| 67c595b440 | |||
| 830b7aee56 | |||
| 1890608cb3 | |||
| bd396959a9 | |||
| d5fe19f0a5 | |||
| 46bfb09415 | |||
| a60cb596d1 | |||
| 6f9d5e2d5f | |||
| 80bc226ee1 | |||
| 12e37184e1 | |||
| 25e9345d02 | |||
| ccc4d030c3 | |||
| 90c0817b6d | |||
| 527f7bbafe | |||
| 765a32729f | |||
| de783f77a2 | |||
| de4c8d104c | |||
| 88c8c28f45 | |||
| 57b36826d1 | |||
| f81a2f309b | |||
| 5fc760f4a5 | |||
| 091cb38339 | |||
| 3ae9b3e576 | |||
| b03b4cbeec | |||
| f2c1b3c76a | |||
| f3bec34882 | |||
| d6aa9fe9c2 | |||
| 2d5304a770 | |||
| 88f2c16c82 | |||
| 490c318d1c | |||
| 8beaf61a08 | |||
| 8b61c984eb | |||
| e38094df58 | |||
| c25e3f5867 | |||
| f78e81d175 | |||
| 3837ae237d | |||
| 2b6ef8b4ff | |||
| 6cc0eefb3e | |||
| ab11e28bf7 | |||
| 26d5f5a5f5 | |||
| 74ae91cba6 | |||
| 70509c7edd | |||
| 5a69bd6c63 | |||
| 091bb1394f | |||
| b82c3864a0 | |||
| 49ee38a936 | |||
| c201866c51 | |||
| 023f841fb5 | |||
| 76102e9ab3 | |||
| b2fc5e2a4d | |||
| 55aacb8753 | |||
| 8702846216 | 
							
								
								
									
										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 | ||||
| local.properties | ||||
| kotlin-js-store | ||||
|  | ||||
| publishing.sh | ||||
|   | ||||
							
								
								
									
										27
									
								
								.travis.yml
									
									
									
									
									
								
							
							
						
						
									
										27
									
								
								.travis.yml
									
									
									
									
									
								
							| @@ -1,27 +0,0 @@ | ||||
| language: android | ||||
| install: true | ||||
|  | ||||
| os: linux | ||||
| dist: trusty | ||||
| jdk: oraclejdk8 | ||||
|  | ||||
| android: | ||||
|   components: | ||||
|     - tools | ||||
|     - platform-tools | ||||
|     - build-tools-30.0.2 | ||||
|     - android-30 | ||||
|     - add-on | ||||
|     - extra | ||||
|  | ||||
| before_script: | ||||
|   - yes | /usr/local/android-sdk/tools/bin/sdkmanager "build-tools;30.0.2" | ||||
|   - yes | /usr/local/android-sdk/tools/bin/sdkmanager "platforms;android-30" | ||||
|  | ||||
| jobs: | ||||
|   include: | ||||
|     - stage: build | ||||
|       script: ./gradlew build -s -x jvmTest -x jsIrTest -x jsIrBrowserTest -x jsIrNodeTest -x jsLegacyTest -x jsLegacyBrowserTest -x jsLegacyNodeTest | ||||
| #    Tests are temporarily disabled on public travis due to the problems of launching | ||||
| #    - state: test | ||||
| #      script: ./gradlew allTests | ||||
							
								
								
									
										1420
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										1420
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										38
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										38
									
								
								README.md
									
									
									
									
									
								
							| @@ -1 +1,37 @@ | ||||
| # MicroUtils | ||||
| # MicroUtils | ||||
|  | ||||
| This is a library with collection of tools for working in Kotlin environment. First of all, this library collection is oriented to use next technologies: | ||||
|  | ||||
| * [`Kotlin Coroutines`](https://github.com/Kotlin/kotlinx.coroutines) | ||||
| * [`Kotlin Serialization`](https://github.com/Kotlin/kotlinx.serialization) | ||||
| * [`Kotlin Exposed`](https://github.com/JetBrains/Exposed) | ||||
| * [`Ktor`](https://ktor.io) | ||||
|  | ||||
| <details> | ||||
|   <summary> <b>Android environment</b> </summary> | ||||
|  | ||||
| You always can look at the <a href="https://github.com/InsanusMokrassar/MicroUtils/blob/master/gradle.properties#L24-L34">properties file</a> to get information about current project dependencies, compile and build tools for `Android` target. | ||||
|  | ||||
| </details> | ||||
|  | ||||
| ## Projects | ||||
|  | ||||
| * `common` contains common tools for platform which usually are absent out-of-the-box when you starting project | ||||
| * `selector` contains tools to use `Selector` interface with things like `RecyclerView` in android or other selection needs | ||||
| * `coroutines` is a module for `Kotlin Coroutines` with different things like subscribing on flows (`onEach` + `launchIn` shortcut :) ) | ||||
| * `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 | ||||
| * `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 | ||||
|  | ||||
| ## Complex modules structure | ||||
|  | ||||
| Most of complex modules are built with next hierarchy: | ||||
|  | ||||
| * `common` submodule for `API` things which are common for all platforms | ||||
| * `exposed` submodule contains realizations for exposed tables | ||||
| * `ktor` submodule is usually unavailable directly, because it contains its own submodules for clients and servers | ||||
|     * `common` part contains routes which are common for clients and servers | ||||
|     * `client` submodule contains clients which are usually using `UnifiedRequester` to make requests using routes from `ktor/common` module and some internal logic of requests | ||||
|     * `server` submodule (in most cases `JVM`-only) contains some extensions for `Route` instances which usually will give opportunity to proxy internet requests from `ktor/client` realization to some proxy object | ||||
|   | ||||
							
								
								
									
										1
									
								
								_config.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								_config.yml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| theme: jekyll-theme-cayman | ||||
| @@ -10,7 +10,7 @@ kotlin { | ||||
|     sourceSets { | ||||
|         androidMain { | ||||
|             dependencies { | ||||
|                 api "androidx.appcompat:appcompat-resources:$appcompat_version" | ||||
|                 api libs.android.appCompat.resources | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|   | ||||
| @@ -15,7 +15,7 @@ data class AlertAction( | ||||
|     val callback: (DialogInterface) -> Unit | ||||
| ) | ||||
|  | ||||
| private class ActionViewHolder( | ||||
| class ActionViewHolder( | ||||
|     container: ViewGroup, dialogInterfaceGetter: () -> DialogInterface | ||||
| ) : AbstractStandardViewHolder<AlertAction>(container, android.R.layout.simple_list_item_1) { | ||||
|     private lateinit var action: AlertAction | ||||
| @@ -34,10 +34,10 @@ private class ActionViewHolder( | ||||
|     } | ||||
| } | ||||
|  | ||||
| private class ActionsRecyclerViewAdapter( | ||||
|     data: List<AlertAction>, | ||||
| class ActionsRecyclerViewAdapter( | ||||
|     override val data: List<AlertAction>, | ||||
|     private val dialogInterfaceGetter: () -> DialogInterface | ||||
| ) : RecyclerViewAdapter<AlertAction>(data) { | ||||
| ) : RecyclerViewAdapter<AlertAction>() { | ||||
|     override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AbstractViewHolder<AlertAction> = ActionViewHolder( | ||||
|         parent, dialogInterfaceGetter | ||||
|     ) | ||||
|   | ||||
| @@ -10,12 +10,13 @@ kotlin { | ||||
|     sourceSets { | ||||
|         commonMain { | ||||
|             dependencies { | ||||
|                 api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version" | ||||
|                 api libs.kt.coroutines | ||||
|                 api project(":micro_utils.common") | ||||
|             } | ||||
|         } | ||||
|         androidMain { | ||||
|             dependencies { | ||||
|                 api "androidx.recyclerview:recyclerview:$androidx_recycler_version" | ||||
|                 api libs.android.recyclerView | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|   | ||||
| @@ -1,12 +1,21 @@ | ||||
| package dev.inmo.micro_utils.android.recyclerview | ||||
|  | ||||
| import android.view.View | ||||
| import android.view.ViewGroup | ||||
| import androidx.recyclerview.widget.RecyclerView | ||||
| import kotlinx.coroutines.flow.* | ||||
|  | ||||
|  | ||||
| abstract class RecyclerViewAdapter<T>( | ||||
|     val data: List<T> | ||||
| ): RecyclerView.Adapter<AbstractViewHolder<T>>() { | ||||
| abstract class RecyclerViewAdapter<T>: RecyclerView.Adapter<AbstractViewHolder<T>>() { | ||||
|     protected abstract val data: List<T> | ||||
|  | ||||
|     private val _dataCountState by lazy { | ||||
|         MutableStateFlow<Int>(data.size) | ||||
|     } | ||||
|     val dataCountState: StateFlow<Int> by lazy { | ||||
|         _dataCountState.asStateFlow() | ||||
|     } | ||||
|  | ||||
|     var emptyView: View? = null | ||||
|         set(value) { | ||||
|             field = value | ||||
| @@ -18,31 +27,37 @@ abstract class RecyclerViewAdapter<T>( | ||||
|             object : RecyclerView.AdapterDataObserver() { | ||||
|                 override fun onItemRangeChanged(positionStart: Int, itemCount: Int) { | ||||
|                     super.onItemRangeChanged(positionStart, itemCount) | ||||
|                     _dataCountState.value = data.size | ||||
|                     checkEmpty() | ||||
|                 } | ||||
|  | ||||
|                 override fun onItemRangeChanged(positionStart: Int, itemCount: Int, payload: Any?) { | ||||
|                     super.onItemRangeChanged(positionStart, itemCount, payload) | ||||
|                     _dataCountState.value = data.size | ||||
|                     checkEmpty() | ||||
|                 } | ||||
|  | ||||
|                 override fun onChanged() { | ||||
|                     super.onChanged() | ||||
|                     _dataCountState.value = data.size | ||||
|                     checkEmpty() | ||||
|                 } | ||||
|  | ||||
|                 override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) { | ||||
|                     super.onItemRangeRemoved(positionStart, itemCount) | ||||
|                     _dataCountState.value = data.size | ||||
|                     checkEmpty() | ||||
|                 } | ||||
|  | ||||
|                 override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) { | ||||
|                     super.onItemRangeMoved(fromPosition, toPosition, itemCount) | ||||
|                     _dataCountState.value = data.size | ||||
|                     checkEmpty() | ||||
|                 } | ||||
|  | ||||
|                 override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { | ||||
|                     super.onItemRangeInserted(positionStart, itemCount) | ||||
|                     _dataCountState.value = data.size | ||||
|                     checkEmpty() | ||||
|                 } | ||||
|             } | ||||
| @@ -58,7 +73,7 @@ abstract class RecyclerViewAdapter<T>( | ||||
|  | ||||
|     private fun checkEmpty() { | ||||
|         emptyView ?. let { | ||||
|             if (data.isEmpty()) { | ||||
|             if (dataCountState.value == 0) { | ||||
|                 it.visibility = View.VISIBLE | ||||
|             } else { | ||||
|                 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) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										25
									
								
								build.gradle
									
									
									
									
									
								
							
							
						
						
									
										25
									
								
								build.gradle
									
									
									
									
									
								
							| @@ -1,6 +1,5 @@ | ||||
| buildscript { | ||||
|     repositories { | ||||
|         jcenter() | ||||
|         google() | ||||
|         mavenCentral() | ||||
|         mavenLocal() | ||||
| @@ -8,22 +7,30 @@ buildscript { | ||||
|     } | ||||
|  | ||||
|     dependencies { | ||||
|         classpath 'com.android.tools.build:gradle:4.0.2' | ||||
|         classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" | ||||
|         classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" | ||||
|         classpath "com.getkeepsafe.dexcount:dexcount-gradle-plugin:$dexcount_version" | ||||
|         classpath "com.github.breadmoirai:github-release:$github_release_plugin_version" | ||||
|         classpath "org.jetbrains.dokka:dokka-gradle-plugin:$dokka_version" | ||||
|         classpath libs.buildscript.kt.gradle | ||||
|         classpath libs.buildscript.kt.serialization | ||||
|         classpath libs.buildscript.jb.dokka | ||||
|         classpath libs.buildscript.gh.release | ||||
|         classpath libs.buildscript.android.gradle | ||||
|         classpath libs.buildscript.android.dexcount | ||||
|     } | ||||
| } | ||||
|  | ||||
| allprojects { | ||||
|     repositories { | ||||
|         mavenLocal() | ||||
|         jcenter() | ||||
|         mavenCentral() | ||||
|         google() | ||||
|         maven { url "https://kotlin.bintray.com/kotlinx" } | ||||
|         maven { url "https://maven.pkg.jetbrains.space/public/p/compose/dev" } | ||||
|     } | ||||
|  | ||||
|     // 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,19 @@ plugins { | ||||
| } | ||||
|  | ||||
| apply from: "$mppProjectWithSerializationPresetPath" | ||||
|  | ||||
| kotlin { | ||||
|     sourceSets { | ||||
|         jvmMain { | ||||
|             dependencies { | ||||
|                 api project(":micro_utils.coroutines") | ||||
|             } | ||||
|         } | ||||
|         androidMain { | ||||
|             dependencies { | ||||
|                 api project(":micro_utils.coroutines") | ||||
|                 api libs.android.fragment | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
							
								
								
									
										18
									
								
								common/compose/build.gradle
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								common/compose/build.gradle
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | ||||
| plugins { | ||||
|     id "org.jetbrains.kotlin.multiplatform" | ||||
|     id "org.jetbrains.kotlin.plugin.serialization" | ||||
|     id "com.android.library" | ||||
|     alias(libs.plugins.jb.compose) | ||||
| } | ||||
|  | ||||
| apply from: "$mppProjectWithSerializationAndComposePresetPath" | ||||
|  | ||||
| kotlin { | ||||
|     sourceSets { | ||||
|         commonMain { | ||||
|             dependencies { | ||||
|                 api project(":micro_utils.common") | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,16 @@ | ||||
| package dev.inmo.micro_utils.common.compose | ||||
|  | ||||
| import androidx.compose.runtime.DisposableEffectResult | ||||
|  | ||||
| class DefaultDisposableEffectResult( | ||||
|     private val onDispose: () -> Unit | ||||
| ) : DisposableEffectResult { | ||||
|     override fun dispose() { | ||||
|         onDispose() | ||||
|     } | ||||
|  | ||||
|     companion object { | ||||
|         val DoNothing = DefaultDisposableEffectResult {} | ||||
|     } | ||||
| } | ||||
|  | ||||
| @@ -0,0 +1,10 @@ | ||||
| package dev.inmo.micro_utils.common.compose | ||||
|  | ||||
| import androidx.compose.runtime.MutableState | ||||
| import androidx.compose.runtime.State | ||||
| import androidx.compose.runtime.derivedStateOf | ||||
|  | ||||
| /** | ||||
|  * Converts current [MutableState] to immutable [State] using [derivedStateOf] | ||||
|  */ | ||||
| fun <T> MutableState<T>.asState(): State<T> = derivedStateOf { this.value } | ||||
| @@ -0,0 +1,9 @@ | ||||
| package dev.inmo.micro_utils.common.compose | ||||
|  | ||||
| import androidx.compose.runtime.Composition | ||||
| import dev.inmo.micro_utils.common.onRemoved | ||||
| import org.w3c.dom.Element | ||||
|  | ||||
| fun Composition.linkWithElement(element: Element) { | ||||
|     element.onRemoved { dispose() } | ||||
| } | ||||
| @@ -0,0 +1,10 @@ | ||||
| package dev.inmo.micro_utils.common.compose | ||||
|  | ||||
| import org.jetbrains.compose.web.attributes.ATarget | ||||
|  | ||||
| fun openLink(link: String, mode: ATarget = ATarget.Blank, features: String = "") = dev.inmo.micro_utils.common.openLink( | ||||
|     link, | ||||
|     mode.targetStr, | ||||
|     features | ||||
| ) | ||||
|  | ||||
| @@ -0,0 +1,13 @@ | ||||
| package dev.inmo.micro_utils.common.compose | ||||
|  | ||||
| import androidx.compose.runtime.* | ||||
| import org.jetbrains.compose.web.dom.DOMScope | ||||
| import org.w3c.dom.Element | ||||
|  | ||||
| fun <TElement : Element> renderComposableAndLinkToRoot( | ||||
|     root: TElement, | ||||
|     monotonicFrameClock: MonotonicFrameClock = DefaultMonotonicFrameClock, | ||||
|     content: @Composable DOMScope<TElement>.() -> Unit | ||||
| ): Composition = org.jetbrains.compose.web.renderComposable(root, monotonicFrameClock, content).apply { | ||||
|     linkWithElement(root) | ||||
| } | ||||
| @@ -0,0 +1,43 @@ | ||||
| package dev.inmo.micro_utils.common.compose | ||||
|  | ||||
| import org.jetbrains.compose.web.css.* | ||||
|  | ||||
| object SkeletonAnimation : StyleSheet() { | ||||
|     val skeletonKeyFrames: CSSNamedKeyframes by keyframes { | ||||
|         to { backgroundPosition("-20% 0") } | ||||
|     } | ||||
|  | ||||
|     fun CSSBuilder.includeSkeletonStyle( | ||||
|         duration: CSSSizeValue<out CSSUnitTime> = 2.s, | ||||
|         timingFunction: AnimationTimingFunction = AnimationTimingFunction.EaseInOut, | ||||
|         iterationCount: Int? = null, | ||||
|         direction: AnimationDirection = AnimationDirection.Normal, | ||||
|         keyFrames: CSSNamedKeyframes = skeletonKeyFrames, | ||||
|         hideChildren: Boolean = true, | ||||
|         hideText: Boolean = hideChildren | ||||
|     ) { | ||||
|         backgroundImage("linear-gradient(110deg, rgb(236, 236, 236) 40%, rgb(245, 245, 245) 50%, rgb(236, 236, 236) 65%)") | ||||
|         backgroundSize("200% 100%") | ||||
|         backgroundPosition("180% 0") | ||||
|         animation(keyFrames) { | ||||
|             duration(duration) | ||||
|             timingFunction(timingFunction) | ||||
|             iterationCount(iterationCount) | ||||
|             direction(direction) | ||||
|         } | ||||
|         if (hideText) { | ||||
|             property("color", "${Color.transparent} !important") | ||||
|         } | ||||
|  | ||||
|         if (hideChildren) { | ||||
|             child(self, universal) style { | ||||
|                 property("visibility", "hidden") | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     val skeleton by style { | ||||
|         includeSkeletonStyle() | ||||
|     } | ||||
| } | ||||
|  | ||||
							
								
								
									
										1
									
								
								common/compose/src/main/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								common/compose/src/main/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| <manifest package="dev.inmo.micro_utils.common.compose"/> | ||||
| @@ -1,3 +1,5 @@ | ||||
| @file:Suppress("OPT_IN_IS_NOT_ENABLED") | ||||
|  | ||||
| package dev.inmo.micro_utils.common | ||||
|  | ||||
| @RequiresOptIn( | ||||
| @@ -12,8 +14,22 @@ package dev.inmo.micro_utils.common | ||||
|     AnnotationTarget.PROPERTY_GETTER, | ||||
|     AnnotationTarget.PROPERTY_SETTER, | ||||
|     AnnotationTarget.FUNCTION, | ||||
|     AnnotationTarget.TYPE, | ||||
|     AnnotationTarget.TYPEALIAS, | ||||
|     AnnotationTarget.TYPE_PARAMETER | ||||
|     AnnotationTarget.TYPEALIAS | ||||
| ) | ||||
| annotation class PreviewFeature | ||||
| annotation class PreviewFeature(val message: String = "It is possible, that behaviour of this thing will be changed or removed in future releases") | ||||
|  | ||||
| @RequiresOptIn( | ||||
|     "This thing is marked as warned. See message of warn to get more info", | ||||
|     RequiresOptIn.Level.WARNING | ||||
| ) | ||||
| @Target( | ||||
|     AnnotationTarget.CLASS, | ||||
|     AnnotationTarget.CONSTRUCTOR, | ||||
|     AnnotationTarget.FIELD, | ||||
|     AnnotationTarget.PROPERTY, | ||||
|     AnnotationTarget.PROPERTY_GETTER, | ||||
|     AnnotationTarget.PROPERTY_SETTER, | ||||
|     AnnotationTarget.FUNCTION, | ||||
|     AnnotationTarget.TYPEALIAS | ||||
| ) | ||||
| annotation class Warning(val message: String) | ||||
|   | ||||
| @@ -2,6 +2,8 @@ | ||||
|  | ||||
| package dev.inmo.micro_utils.common | ||||
|  | ||||
| import kotlinx.serialization.Serializable | ||||
|  | ||||
| private inline fun <T> getObject( | ||||
|     additional: MutableList<T>, | ||||
|     iterator: Iterator<T> | ||||
| @@ -14,21 +16,30 @@ private inline fun <T> getObject( | ||||
| /** | ||||
|  * Diff object which contains information about differences between two [Iterable]s | ||||
|  * | ||||
|  * See tests for more info | ||||
|  * | ||||
|  * @param removed The objects which has been presented in the old collection but absent in new one. Index here is the index in the old collection | ||||
|  * @param added The object which appear in new collection only. Indexes here show the index in the new collection | ||||
|  * @param replaced Pair of old-new changes. First object has been presented in the old collection on its | ||||
|  * [IndexedValue.index] place, the second one is the object in new collection. Both have indexes due to the fact that in | ||||
|  * case when some value has been replaced after adds or removes in original collection the object index will be changed | ||||
|  * | ||||
|  * @see calculateDiff | ||||
|  */ | ||||
| @Serializable | ||||
| data class Diff<T> internal constructor( | ||||
|     val removed: List<IndexedValue<T>>, | ||||
|     val removed: List<@Serializable(IndexedValueSerializer::class) IndexedValue<T>>, | ||||
|     /** | ||||
|      * Old-New values pairs | ||||
|      */ | ||||
|     val replaced: List<Pair<IndexedValue<T>, IndexedValue<T>>>, | ||||
|     val added: List<IndexedValue<T>> | ||||
|     val replaced: List<Pair<@Serializable(IndexedValueSerializer::class) IndexedValue<T>, @Serializable(IndexedValueSerializer::class) IndexedValue<T>>>, | ||||
|     val added: List<@Serializable(IndexedValueSerializer::class) IndexedValue<T>> | ||||
| ) | ||||
|  | ||||
| private inline fun <T> performChanges( | ||||
|     potentialChanges: MutableList<Pair<IndexedValue<T>?, IndexedValue<T>?>>, | ||||
|     additionalsInOld: MutableList<T>, | ||||
|     additionalsInNew: MutableList<T>, | ||||
|     additionsInOld: MutableList<T>, | ||||
|     additionsInNew: MutableList<T>, | ||||
|     changedList: MutableList<Pair<IndexedValue<T>, IndexedValue<T>>>, | ||||
|     removedList: MutableList<IndexedValue<T>>, | ||||
|     addedList: MutableList<IndexedValue<T>>, | ||||
| @@ -43,6 +54,7 @@ private inline fun <T> performChanges( | ||||
|         if (oldOneEqualToNewObject || newOneEqualToOldObject) { | ||||
|             changedList.addAll( | ||||
|                 potentialChanges.take(i).mapNotNull { | ||||
|                     @Suppress("UNCHECKED_CAST") | ||||
|                     if (it.first != null && it.second != null) it as Pair<IndexedValue<T>, IndexedValue<T>> else null | ||||
|                 } | ||||
|             ) | ||||
| @@ -52,20 +64,20 @@ private inline fun <T> performChanges( | ||||
|                     newPotentials.first().second ?.let { addedList.add(it) } | ||||
|                     newPotentials.drop(1).take(newPotentials.size - 2).forEach { (oldOne, newOne) -> | ||||
|                         addedList.add(newOne!!) | ||||
|                         oldOne ?.let { additionalsInOld.add(oldOne.value) } | ||||
|                         oldOne ?.let { additionsInOld.add(oldOne.value) } | ||||
|                     } | ||||
|                     if (newPotentials.size > 1) { | ||||
|                         newPotentials.last().first ?.value ?.let { additionalsInOld.add(it) } | ||||
|                         newPotentials.last().first ?.value ?.let { additionsInOld.add(it) } | ||||
|                     } | ||||
|                 } | ||||
|                 newOneEqualToOldObject -> { | ||||
|                     newPotentials.first().first ?.let { removedList.add(it) } | ||||
|                     newPotentials.drop(1).take(newPotentials.size - 2).forEach { (oldOne, newOne) -> | ||||
|                         removedList.add(oldOne!!) | ||||
|                         newOne ?.let { additionalsInNew.add(newOne.value) } | ||||
|                         newOne ?.let { additionsInNew.add(newOne.value) } | ||||
|                     } | ||||
|                     if (newPotentials.size > 1) { | ||||
|                         newPotentials.last().second ?.value ?.let { additionalsInNew.add(it) } | ||||
|                         newPotentials.last().second ?.value ?.let { additionsInNew.add(it) } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
| @@ -121,7 +133,10 @@ fun <T> Iterable<T>.calculateDiff( | ||||
|  | ||||
|         when { | ||||
|             oldObject === newObject || (oldObject == newObject && !strictComparison) -> { | ||||
|                 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() | ||||
|             } | ||||
|             else -> { | ||||
| @@ -139,6 +154,10 @@ fun <T> Iterable<T>.calculateDiff( | ||||
|  | ||||
|     return Diff(removedObjects.toList(), changedObjects.toList(), addedObjects.toList()) | ||||
| } | ||||
| inline fun <T> Iterable<T>.diff( | ||||
|     other: Iterable<T>, | ||||
|     strictComparison: Boolean = false | ||||
| ): Diff<T> = calculateDiff(other, strictComparison) | ||||
|  | ||||
| inline fun <T> Diff(old: Iterable<T>, new: Iterable<T>) = old.calculateDiff(new) | ||||
| inline fun <T> StrictDiff(old: Iterable<T>, new: Iterable<T>) = old.calculateDiff(new, true) | ||||
| @@ -149,3 +168,22 @@ inline fun <T> StrictDiff(old: Iterable<T>, new: Iterable<T>) = old.calculateDif | ||||
| inline fun <T> Iterable<T>.calculateStrictDiff( | ||||
|     other: Iterable<T> | ||||
| ) = calculateDiff(other, strictComparison = true) | ||||
|  | ||||
| /** | ||||
|  * This method call [calculateDiff] with strict mode [strictComparison] and then apply differences to [this] | ||||
|  * mutable list | ||||
|  */ | ||||
| fun <T> MutableList<T>.applyDiff( | ||||
|     source: Iterable<T>, | ||||
|     strictComparison: Boolean = false | ||||
| ): Diff<T> = calculateDiff(source, strictComparison).also { | ||||
|     for (i in it.removed.indices.sortedDescending()) { | ||||
|         removeAt(it.removed[i].index) | ||||
|     } | ||||
|     it.added.forEach { (i, t) -> | ||||
|         add(i, t) | ||||
|     } | ||||
|     it.replaced.forEach { (_, new) -> | ||||
|         set(new.index, new.value) | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,160 @@ | ||||
| @file:Suppress("unused", "NOTHING_TO_INLINE") | ||||
|  | ||||
| package dev.inmo.micro_utils.common | ||||
|  | ||||
| import kotlinx.serialization.* | ||||
| import kotlinx.serialization.builtins.serializer | ||||
| import kotlinx.serialization.descriptors.* | ||||
| import kotlinx.serialization.encoding.* | ||||
|  | ||||
| /** | ||||
|  * Realization of this interface will contains at least one not null - [optionalT1] or [optionalT2] | ||||
|  * | ||||
|  * @see EitherFirst | ||||
|  * @see EitherSecond | ||||
|  * @see Either.Companion.first | ||||
|  * @see Either.Companion.second | ||||
|  * @see Either.onFirst | ||||
|  * @see Either.onSecond | ||||
|  * @see Either.mapOnFirst | ||||
|  * @see Either.mapOnSecond | ||||
|  */ | ||||
| @Serializable(EitherSerializer::class) | ||||
| sealed interface Either<T1, T2> { | ||||
|     val optionalT1: Optional<T1> | ||||
|     val optionalT2: Optional<T2> | ||||
|  | ||||
|     val t1OrNull: T1? | ||||
|         get() = optionalT1.dataOrNull() | ||||
|     val t2OrNull: T2? | ||||
|         get() = optionalT2.dataOrNull() | ||||
| } | ||||
|  | ||||
| class EitherSerializer<T1, T2>( | ||||
|     t1Serializer: KSerializer<T1>, | ||||
|     t2Serializer: KSerializer<T2>, | ||||
| ) : KSerializer<Either<T1, T2>> { | ||||
|     @OptIn(InternalSerializationApi::class, ExperimentalSerializationApi::class) | ||||
|     override val descriptor: SerialDescriptor = buildSerialDescriptor( | ||||
|         "TypedSerializer", | ||||
|         SerialKind.CONTEXTUAL | ||||
|     ) { | ||||
|         element("type", String.serializer().descriptor) | ||||
|         element("value", ContextualSerializer(Either::class).descriptor) | ||||
|     } | ||||
|     private val t1EitherSerializer = EitherFirst.serializer(t1Serializer, t2Serializer) | ||||
|     private val t2EitherSerializer = EitherSecond.serializer(t1Serializer, t2Serializer) | ||||
|  | ||||
|     override fun deserialize(decoder: Decoder): Either<T1, T2> { | ||||
|         return decoder.decodeStructure(descriptor) { | ||||
|             var type: String? = null | ||||
|             lateinit var result: Either<T1, T2> | ||||
|             while (true) { | ||||
|                 when (val index = decodeElementIndex(descriptor)) { | ||||
|                     0 -> type = decodeStringElement(descriptor, 0) | ||||
|                     1 -> { | ||||
|                         result = when (type) { | ||||
|                             "t1" -> decodeSerializableElement( | ||||
|                                 descriptor, | ||||
|                                 1, | ||||
|                                 t1EitherSerializer | ||||
|                             ) | ||||
|                             "t2" -> decodeSerializableElement( | ||||
|                                 descriptor, | ||||
|                                 1, | ||||
|                                 t2EitherSerializer | ||||
|                             ) | ||||
|                             else -> error("Unknown type of either: $type") | ||||
|                         } | ||||
|                     } | ||||
|                     CompositeDecoder.DECODE_DONE -> break | ||||
|                     else -> error("Unexpected index: $index") | ||||
|                 } | ||||
|             } | ||||
|             result | ||||
|         } | ||||
|     } | ||||
|  | ||||
|  | ||||
|     override fun serialize(encoder: Encoder, value: Either<T1, T2>) { | ||||
|         encoder.encodeStructure(descriptor) { | ||||
|             when (value) { | ||||
|                 is EitherFirst -> { | ||||
|                     encodeStringElement(descriptor, 0, "t1") | ||||
|                     encodeSerializableElement(descriptor, 1, t1EitherSerializer, value) | ||||
|                 } | ||||
|                 is EitherSecond -> { | ||||
|                     encodeStringElement(descriptor, 0, "t2") | ||||
|                     encodeSerializableElement(descriptor, 1, t2EitherSerializer, value) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * This type [Either] will always have not nullable [optionalT1] | ||||
|  */ | ||||
| @Serializable | ||||
| data class EitherFirst<T1, T2>( | ||||
|     val t1: T1 | ||||
| ) : Either<T1, T2> { | ||||
|     override val optionalT1: Optional<T1> = t1.optional | ||||
|     override val optionalT2: Optional<T2> = Optional.absent() | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * This type [Either] will always have not nullable [optionalT2] | ||||
|  */ | ||||
| @Serializable | ||||
| data class EitherSecond<T1, T2>( | ||||
|     val t2: T2 | ||||
| ) : Either<T1, T2> { | ||||
|     override val optionalT1: Optional<T1> = Optional.absent() | ||||
|     override val optionalT2: Optional<T2> = t2.optional | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * @return New instance of [EitherFirst] | ||||
|  */ | ||||
| inline fun <T1, T2> Either.Companion.first(t1: T1): Either<T1, T2> = EitherFirst(t1) | ||||
| /** | ||||
|  * @return New instance of [EitherSecond] | ||||
|  */ | ||||
| inline fun <T1, T2> Either.Companion.second(t2: T2): Either<T1, T2> = EitherSecond(t2) | ||||
|  | ||||
| /** | ||||
|  * Will call [block] in case when [this] is [EitherFirst] | ||||
|  */ | ||||
| inline fun <T1, T2, E : Either<T1, T2>> E.onFirst(block: (T1) -> Unit): E { | ||||
|     optionalT1.onPresented(block) | ||||
|     return this | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Will call [block] in case when [this] is [EitherSecond] | ||||
|  */ | ||||
| inline fun <T1, T2, E : Either<T1, T2>> E.onSecond(block: (T2) -> Unit): E { | ||||
|     optionalT2.onPresented(block) | ||||
|     return this | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * @return Result of [block] if [this] is [EitherFirst] | ||||
|  */ | ||||
| inline fun <T1, R> Either<T1, *>.mapOnFirst(block: (T1) -> R): R? { | ||||
|     return optionalT1.mapOnPresented(block) | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * @return Result of [block] if [this] is [EitherSecond] | ||||
|  */ | ||||
| inline fun <T2, R> Either<*, T2>.mapOnSecond(block: (T2) -> R): R? { | ||||
|     return optionalT2.mapOnPresented(block) | ||||
| } | ||||
|  | ||||
| inline fun <reified T1, reified T2> Any.either() = when (this) { | ||||
|     is T1 -> Either.first<T1, T2>(this) | ||||
|     is T2 -> Either.second<T1, T2>(this) | ||||
|     else -> error("Incorrect type of either argument $this") | ||||
| } | ||||
| @@ -0,0 +1,43 @@ | ||||
| package dev.inmo.micro_utils.common | ||||
|  | ||||
| inline fun <T> Boolean.letIfTrue(block: () -> T): T? { | ||||
|     return if (this) { | ||||
|         block() | ||||
|     } else { | ||||
|         null | ||||
|     } | ||||
| } | ||||
|  | ||||
| inline fun <T> Boolean.letIfFalse(block: () -> T): T? { | ||||
|     return if (this) { | ||||
|         null | ||||
|     } else { | ||||
|         block() | ||||
|     } | ||||
| } | ||||
|  | ||||
| inline fun Boolean.alsoIfTrue(block: () -> Unit): Boolean { | ||||
|     letIfTrue(block) | ||||
|     return this | ||||
| } | ||||
|  | ||||
| inline fun Boolean.alsoIfFalse(block: () -> Unit): Boolean { | ||||
|     letIfFalse(block) | ||||
|     return this | ||||
| } | ||||
|  | ||||
| inline fun <T> Boolean.ifTrue(block: () -> T): T? { | ||||
|     return if (this) { | ||||
|         block() | ||||
|     } else { | ||||
|         null | ||||
|     } | ||||
| } | ||||
|  | ||||
| inline fun <T> Boolean.ifFalse(block: () -> T): T? { | ||||
|     return if (this) { | ||||
|         null | ||||
|     } else { | ||||
|         block() | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,30 @@ | ||||
| package dev.inmo.micro_utils.common | ||||
|  | ||||
| import kotlinx.serialization.KSerializer | ||||
| import kotlinx.serialization.Serializer | ||||
| import kotlinx.serialization.builtins.PairSerializer | ||||
| import kotlinx.serialization.builtins.serializer | ||||
| import kotlinx.serialization.descriptors.SerialDescriptor | ||||
| import kotlinx.serialization.encoding.Decoder | ||||
| import kotlinx.serialization.encoding.Encoder | ||||
|  | ||||
| class IndexedValueSerializer<T>(private val subSerializer: KSerializer<T>) : KSerializer<IndexedValue<T>> { | ||||
|     private val originalSerializer = PairSerializer(Int.serializer(), subSerializer) | ||||
|     override val descriptor: SerialDescriptor | ||||
|         get() = originalSerializer.descriptor | ||||
|  | ||||
|     override fun deserialize(decoder: Decoder): IndexedValue<T> { | ||||
|         val pair = originalSerializer.deserialize(decoder) | ||||
|         return IndexedValue( | ||||
|             pair.first, | ||||
|             pair.second | ||||
|         ) | ||||
|     } | ||||
|  | ||||
|     override fun serialize(encoder: Encoder, value: IndexedValue<T>) { | ||||
|         originalSerializer.serialize( | ||||
|             encoder, | ||||
|             Pair(value.index, value.value) | ||||
|         ) | ||||
|     } | ||||
| } | ||||
| @@ -7,9 +7,17 @@ import kotlinx.serialization.encoding.Decoder | ||||
| import kotlinx.serialization.encoding.Encoder | ||||
|  | ||||
| typealias ByteArrayAllocator = () -> ByteArray | ||||
| typealias SuspendByteArrayAllocator = suspend () -> ByteArray | ||||
|  | ||||
| val ByteArray.asAllocator: ByteArrayAllocator | ||||
|     get() = { this } | ||||
| val ByteArray.asSuspendAllocator: SuspendByteArrayAllocator | ||||
|     get() = { this } | ||||
| val ByteArrayAllocator.asSuspendAllocator: SuspendByteArrayAllocator | ||||
|     get() = { this() } | ||||
| suspend fun SuspendByteArrayAllocator.asAllocator(): ByteArrayAllocator { | ||||
|     return invoke().asAllocator | ||||
| } | ||||
|  | ||||
| object ByteArrayAllocatorSerializer : KSerializer<ByteArrayAllocator> { | ||||
|     private val realSerializer = ByteArraySerializer() | ||||
| @@ -17,7 +25,7 @@ object ByteArrayAllocatorSerializer : KSerializer<ByteArrayAllocator> { | ||||
|  | ||||
|     override fun deserialize(decoder: Decoder): ByteArrayAllocator { | ||||
|         val bytes = realSerializer.deserialize(decoder) | ||||
|         return { bytes } | ||||
|         return bytes.asAllocator | ||||
|     } | ||||
|  | ||||
|     override fun serialize(encoder: Encoder, value: ByteArrayAllocator) { | ||||
|   | ||||
| @@ -0,0 +1,3 @@ | ||||
| package dev.inmo.micro_utils.common | ||||
|  | ||||
| fun <T> Iterable<T?>.firstNotNull() = first { it != null }!! | ||||
| @@ -0,0 +1,59 @@ | ||||
| package dev.inmo.micro_utils.common | ||||
|  | ||||
| inline fun <I, R> Iterable<I>.joinTo( | ||||
|     separatorFun: (I) -> R?, | ||||
|     prefix: R? = null, | ||||
|     postfix: R? = null, | ||||
|     transform: (I) -> R? | ||||
| ): List<R> { | ||||
|     val result = mutableListOf<R>() | ||||
|     val iterator = iterator() | ||||
|  | ||||
|     prefix ?.let(result::add) | ||||
|  | ||||
|     while (iterator.hasNext()) { | ||||
|         val element = iterator.next() | ||||
|         result.add(transform(element) ?: continue) | ||||
|  | ||||
|         if (iterator.hasNext()) { | ||||
|             result.add(separatorFun(element) ?: continue) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     postfix ?.let(result::add) | ||||
|  | ||||
|     return result | ||||
| } | ||||
|  | ||||
| inline fun <I, R> Iterable<I>.joinTo( | ||||
|     separator: R? = null, | ||||
|     prefix: R? = null, | ||||
|     postfix: R? = null, | ||||
|     transform: (I) -> R? | ||||
| ): List<R> = joinTo({ separator }, prefix, postfix, transform) | ||||
|  | ||||
| inline fun <I> Iterable<I>.joinTo( | ||||
|     separatorFun: (I) -> I?, | ||||
|     prefix: I? = null, | ||||
|     postfix: I? = null | ||||
| ): List<I> = joinTo<I, I>(separatorFun, prefix, postfix) { it } | ||||
|  | ||||
| inline fun <I> Iterable<I>.joinTo( | ||||
|     separator: I? = null, | ||||
|     prefix: I? = null, | ||||
|     postfix: I? = null | ||||
| ): List<I> = joinTo<I>({ separator }, prefix, postfix) | ||||
|  | ||||
| inline fun <I, reified R> Array<I>.joinTo( | ||||
|     separatorFun: (I) -> R?, | ||||
|     prefix: R? = null, | ||||
|     postfix: R? = null, | ||||
|     transform: (I) -> R? | ||||
| ): Array<R> = asIterable().joinTo(separatorFun, prefix, postfix, transform).toTypedArray() | ||||
|  | ||||
| inline fun <I, reified R> Array<I>.joinTo( | ||||
|     separator: R? = null, | ||||
|     prefix: R? = null, | ||||
|     postfix: R? = null, | ||||
|     transform: (I) -> R? | ||||
| ): Array<R> = asIterable().joinTo(separator, prefix, postfix, transform).toTypedArray() | ||||
| @@ -0,0 +1,34 @@ | ||||
| package dev.inmo.micro_utils.common | ||||
|  | ||||
| import kotlinx.serialization.Serializable | ||||
| import kotlin.jvm.JvmInline | ||||
|  | ||||
| @Serializable | ||||
| @JvmInline | ||||
| value class FileName(val string: String) { | ||||
|     val name: String | ||||
|         get() = withoutSlashAtTheEnd.takeLastWhile { it != '/' } | ||||
|     val extension: String | ||||
|         get() = name.takeLastWhile { it != '.' } | ||||
|     val nameWithoutExtension: String | ||||
|         get() { | ||||
|             val filename = name | ||||
|             return filename.indexOfLast { it == '.' }.takeIf { it > -1 } ?.let { | ||||
|                 filename.substring(0, it) | ||||
|             } ?: filename | ||||
|         } | ||||
|     val withoutSlashAtTheEnd: String | ||||
|         get() = string.dropLastWhile { it == '/' } | ||||
|     override fun toString(): String = string | ||||
| } | ||||
|  | ||||
|  | ||||
| expect class MPPFile | ||||
|  | ||||
| expect val MPPFile.filename: FileName | ||||
| expect val MPPFile.filesize: Long | ||||
| expect val MPPFile.bytesAllocatorSync: ByteArrayAllocator | ||||
| expect val MPPFile.bytesAllocator: SuspendByteArrayAllocator | ||||
| fun MPPFile.bytesSync() = bytesAllocatorSync() | ||||
| suspend fun MPPFile.bytes() = bytesAllocator() | ||||
|  | ||||
| @@ -0,0 +1,53 @@ | ||||
| package dev.inmo.micro_utils.common | ||||
|  | ||||
| import kotlin.jvm.JvmName | ||||
|  | ||||
| interface SimpleMapper<T1, T2> { | ||||
|     fun convertToT1(from: T2): T1 | ||||
|     fun convertToT2(from: T1): T2 | ||||
| } | ||||
|  | ||||
| @JvmName("convertFromT2") | ||||
| fun <T1, T2> SimpleMapper<T1, T2>.convert(from: T2) = convertToT1(from) | ||||
| @JvmName("convertFromT1") | ||||
| fun <T1, T2> SimpleMapper<T1, T2>.convert(from: T1) = convertToT2(from) | ||||
|  | ||||
| class SimpleMapperImpl<T1, T2>( | ||||
|     private val t1: (T2) -> T1, | ||||
|     private val t2: (T1) -> T2, | ||||
| ) : SimpleMapper<T1, T2> { | ||||
|     override fun convertToT1(from: T2): T1 = t1.invoke(from) | ||||
|  | ||||
|     override fun convertToT2(from: T1): T2 = t2.invoke(from) | ||||
| } | ||||
|  | ||||
| @Suppress("NOTHING_TO_INLINE") | ||||
| inline fun <T1, T2> simpleMapper( | ||||
|     noinline t1: (T2) -> T1, | ||||
|     noinline t2: (T1) -> T2, | ||||
| ) = SimpleMapperImpl(t1, t2) | ||||
|  | ||||
| interface SimpleSuspendableMapper<T1, T2> { | ||||
|     suspend fun convertToT1(from: T2): T1 | ||||
|     suspend fun convertToT2(from: T1): T2 | ||||
| } | ||||
|  | ||||
| @JvmName("convertFromT2") | ||||
| suspend fun <T1, T2> SimpleSuspendableMapper<T1, T2>.convert(from: T2) = convertToT1(from) | ||||
| @JvmName("convertFromT1") | ||||
| suspend fun <T1, T2> SimpleSuspendableMapper<T1, T2>.convert(from: T1) = convertToT2(from) | ||||
|  | ||||
| class SimpleSuspendableMapperImpl<T1, T2>( | ||||
|     private val t1: suspend (T2) -> T1, | ||||
|     private val t2: suspend (T1) -> T2, | ||||
| ) : SimpleSuspendableMapper<T1, T2> { | ||||
|     override suspend fun convertToT1(from: T2): T1 = t1.invoke(from) | ||||
|  | ||||
|     override suspend fun convertToT2(from: T1): T2 = t2.invoke(from) | ||||
| } | ||||
|  | ||||
| @Suppress("NOTHING_TO_INLINE") | ||||
| inline fun <T1, T2> simpleSuspendableMapper( | ||||
|     noinline t1: suspend (T2) -> T1, | ||||
|     noinline t2: suspend (T1) -> T2, | ||||
| ) = SimpleSuspendableMapperImpl(t1, t2) | ||||
| @@ -0,0 +1,97 @@ | ||||
| @file:Suppress("unused") | ||||
|  | ||||
| package dev.inmo.micro_utils.common | ||||
|  | ||||
| import kotlinx.serialization.Serializable | ||||
|  | ||||
| /** | ||||
|  * This type represents [T] as not only potentially nullable data, but also as a data which can not be presented. This | ||||
|  * type will be useful in cases when [T] is nullable and null as valuable data too in time of data absence should be | ||||
|  * presented by some third type. | ||||
|  * | ||||
|  * Let's imagine, you have nullable name in some database. In case when name is not nullable everything is clear - null | ||||
|  * will represent absence of row in the database. In case when name is nullable null will be a little bit dual-meaning, | ||||
|  * cause this null will say nothing about availability of the row (of course, it is exaggerated example) | ||||
|  * | ||||
|  * @see Optional.presented | ||||
|  * @see Optional.absent | ||||
|  * @see Optional.optional | ||||
|  * @see Optional.onPresented | ||||
|  * @see Optional.onAbsent | ||||
|  */ | ||||
| @Serializable | ||||
| data class Optional<T> internal constructor( | ||||
|     @Warning("It is unsafe to use this data directly") | ||||
|     val data: T?, | ||||
|     @Warning("It is unsafe to use this data directly") | ||||
|     val dataPresented: Boolean | ||||
| ) { | ||||
|     companion object { | ||||
|         /** | ||||
|          * Will create [Optional] with presented data | ||||
|          */ | ||||
|         fun <T> presented(data: T) = Optional(data, true) | ||||
|         /** | ||||
|          * Will create [Optional] without data | ||||
|          */ | ||||
|         fun <T> absent() = Optional<T>(null, false) | ||||
|     } | ||||
| } | ||||
|  | ||||
| inline val <T> T.optional | ||||
|     get() = Optional.presented(this) | ||||
|  | ||||
| inline val <T : Any> T?.optionalOrAbsentIfNull | ||||
|     get() = if (this == null) { | ||||
|         Optional.absent<T>() | ||||
|     } else { | ||||
|         Optional.presented(this) | ||||
|     } | ||||
|  | ||||
| /** | ||||
|  * Will call [block] when data presented ([Optional.dataPresented] == true) | ||||
|  */ | ||||
| inline fun <T> Optional<T>.onPresented(block: (T) -> Unit): Optional<T> = apply { | ||||
|     @OptIn(Warning::class) | ||||
|     if (dataPresented) { @Suppress("UNCHECKED_CAST") block(data as T) } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Will call [block] when data presented ([Optional.dataPresented] == true) | ||||
|  */ | ||||
| inline fun <T, R> Optional<T>.mapOnPresented(block: (T) -> R): R? = run { | ||||
|     @OptIn(Warning::class) | ||||
|     if (dataPresented) { @Suppress("UNCHECKED_CAST") block(data as T) } else null | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Will call [block] when data absent ([Optional.dataPresented] == false) | ||||
|  */ | ||||
| inline fun <T> Optional<T>.onAbsent(block: () -> Unit): Optional<T> = apply { | ||||
|     @OptIn(Warning::class) | ||||
|     if (!dataPresented) { block() } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Will call [block] when data presented ([Optional.dataPresented] == true) | ||||
|  */ | ||||
| inline fun <T, R> Optional<T>.mapOnAbsent(block: () -> R): R? = run { | ||||
|     @OptIn(Warning::class) | ||||
|     if (!dataPresented) { block() } else null | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Returns [Optional.data] if [Optional.dataPresented] of [this] is true, or null otherwise | ||||
|  */ | ||||
| fun <T> Optional<T>.dataOrNull() = @OptIn(Warning::class) if (dataPresented) @Suppress("UNCHECKED_CAST") (data as T) else null | ||||
|  | ||||
| /** | ||||
|  * Returns [Optional.data] if [Optional.dataPresented] of [this] is true, or throw [throwable] otherwise | ||||
|  */ | ||||
| fun <T> Optional<T>.dataOrThrow(throwable: Throwable) = @OptIn(Warning::class) if (dataPresented) @Suppress("UNCHECKED_CAST") (data as T) else throw throwable | ||||
|  | ||||
|  | ||||
| /** | ||||
|  * Returns [Optional.data] if [Optional.dataPresented] of [this] is true, or call [block] and returns the result of it | ||||
|  */ | ||||
| inline fun <T> Optional<T>.dataOrElse(block: () -> T) = @OptIn(Warning::class) if (dataPresented) @Suppress("UNCHECKED_CAST") (data as T) else block() | ||||
| @@ -0,0 +1,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() | ||||
|     } | ||||
| } | ||||
| @@ -11,7 +11,7 @@ class DiffUtilsTests { | ||||
|         val withIndex = oldList.withIndex() | ||||
|  | ||||
|         for (count in 1 .. (floor(oldList.size.toFloat() / 2).toInt())) { | ||||
|             for ((i, v) in withIndex) { | ||||
|             for ((i, _) in withIndex) { | ||||
|                 if (i + count > oldList.lastIndex) { | ||||
|                     continue | ||||
|                 } | ||||
| @@ -32,7 +32,7 @@ class DiffUtilsTests { | ||||
|         val withIndex = oldList.withIndex() | ||||
|  | ||||
|         for (count in 1 .. (floor(oldList.size.toFloat() / 2).toInt())) { | ||||
|             for ((i, v) in withIndex) { | ||||
|             for ((i, _) in withIndex) { | ||||
|                 if (i + count > oldList.lastIndex) { | ||||
|                     continue | ||||
|                 } | ||||
| @@ -54,8 +54,8 @@ class DiffUtilsTests { | ||||
|         val oldList = (0 until 10).map { it.toString() } | ||||
|         val withIndex = oldList.withIndex() | ||||
|  | ||||
|         for (step in 0 until oldList.size) { | ||||
|             for ((i, v) in withIndex) { | ||||
|         for (step in oldList.indices) { | ||||
|             for ((i, _) in withIndex) { | ||||
|                 val mutable = oldList.toMutableList() | ||||
|                 val changes = ( | ||||
|                     if (step == 0) i until oldList.size else (i until oldList.size step step) | ||||
| @@ -73,4 +73,83 @@ class DiffUtilsTests { | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @Test | ||||
|     fun testThatSimpleRemoveApplyWorks() { | ||||
|         val oldList = (0 until 10).toList() | ||||
|         val withIndex = oldList.withIndex() | ||||
|  | ||||
|         for (count in 1 .. (floor(oldList.size.toFloat() / 2).toInt())) { | ||||
|             for ((i, _) in withIndex) { | ||||
|                 if (i + count > oldList.lastIndex) { | ||||
|                     continue | ||||
|                 } | ||||
|                 val removedSublist = oldList.subList(i, i + count) | ||||
|                 val mutableOldList = oldList.toMutableList() | ||||
|                 val targetList = oldList - removedSublist | ||||
|  | ||||
|                 mutableOldList.applyDiff(targetList) | ||||
|  | ||||
|                 assertEquals( | ||||
|                     targetList, | ||||
|                     mutableOldList | ||||
|                 ) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @Test | ||||
|     fun testThatSimpleAddApplyWorks() { | ||||
|         val oldList = (0 until 10).map { it.toString() } | ||||
|         val withIndex = oldList.withIndex() | ||||
|  | ||||
|         for (count in 1 .. (floor(oldList.size.toFloat() / 2).toInt())) { | ||||
|             for ((i, _) in withIndex) { | ||||
|                 if (i + count > oldList.lastIndex) { | ||||
|                     continue | ||||
|                 } | ||||
|                 val addedSublist = oldList.subList(i, i + count).map { "added$it" } | ||||
|                 val mutable = oldList.toMutableList() | ||||
|                 mutable.addAll(i, addedSublist) | ||||
|                 val mutableOldList = oldList.toMutableList() | ||||
|  | ||||
|                 mutableOldList.applyDiff(mutable) | ||||
|  | ||||
|                 assertEquals( | ||||
|                     mutable, | ||||
|                     mutableOldList | ||||
|                 ) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @Test | ||||
|     fun testThatSimpleChangesApplyWorks() { | ||||
|         val oldList = (0 until 10).map { it.toString() } | ||||
|         val withIndex = oldList.withIndex() | ||||
|  | ||||
|         for (step in oldList.indices) { | ||||
|             for ((i, _) in withIndex) { | ||||
|                 val mutable = oldList.toMutableList() | ||||
|  | ||||
|                 val newList = if (step == 0) { | ||||
|                     i until oldList.size | ||||
|                 } else { | ||||
|                     i until oldList.size step step | ||||
|                 } | ||||
|                 newList.forEach { index -> | ||||
|                     IndexedValue(index, mutable[index]) to IndexedValue(index, "changed$index").also { | ||||
|                         mutable[index] = it.value | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 val mutableOldList = oldList.toMutableList() | ||||
|                 mutableOldList.applyDiff(mutable) | ||||
|                 assertEquals( | ||||
|                     mutable, | ||||
|                     mutableOldList | ||||
|                 ) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,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,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,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 | ||||
| @@ -5,23 +5,44 @@ import android.view.ViewGroup | ||||
| import android.view.animation.Animation | ||||
| import android.view.animation.Transformation | ||||
|  | ||||
| @PreviewFeature | ||||
| fun View.expand( | ||||
| private fun View.performExpand( | ||||
|     duration: Long = 500, | ||||
|     targetWidth: Int = ViewGroup.LayoutParams.MATCH_PARENT, | ||||
|     targetHeight: Int = ViewGroup.LayoutParams.WRAP_CONTENT | ||||
|     targetHeight: Int = ViewGroup.LayoutParams.WRAP_CONTENT, | ||||
|     onMeasured: View.() -> Unit, | ||||
|     onPerformAnimation: View.(interpolatedTime: Float, t: Transformation?) -> Unit | ||||
| ) { | ||||
|     measure(targetWidth, targetHeight) | ||||
|     val measuredHeight: Int = measuredHeight | ||||
|     layoutParams.height = 0 | ||||
|     visibility = View.VISIBLE | ||||
|     onMeasured() | ||||
|     show() | ||||
|     val a: Animation = object : Animation() { | ||||
|         override fun applyTransformation(interpolatedTime: Float, t: Transformation?) { | ||||
|             super.applyTransformation(interpolatedTime, t) | ||||
|             layoutParams.height = if (interpolatedTime == 1f) targetHeight else (measuredHeight * interpolatedTime).toInt() | ||||
|             onPerformAnimation(interpolatedTime, t) | ||||
|             requestLayout() | ||||
|         } | ||||
|  | ||||
|         override fun willChangeBounds(): Boolean = true | ||||
|     } | ||||
|  | ||||
|     a.duration = duration | ||||
|     startAnimation(a) | ||||
| } | ||||
|  | ||||
| private fun View.performCollapse( | ||||
|     duration: Long = 500, | ||||
|     onPerformAnimation: View.(interpolatedTime: Float, t: Transformation?) -> Unit | ||||
| ) { | ||||
|     val a: Animation = object : Animation() { | ||||
|         override fun applyTransformation(interpolatedTime: Float, t: Transformation?) { | ||||
|             if (interpolatedTime == 1f) { | ||||
|                 gone() | ||||
|             } else { | ||||
|                 onPerformAnimation(interpolatedTime, t) | ||||
|                 requestLayout() | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         override fun willChangeBounds(): Boolean { | ||||
|             return true | ||||
|         } | ||||
| @@ -32,27 +53,58 @@ fun View.expand( | ||||
|     startAnimation(a) | ||||
| } | ||||
|  | ||||
| @PreviewFeature | ||||
| fun View.expand( | ||||
|     duration: Long = 500, | ||||
|     targetWidth: Int = ViewGroup.LayoutParams.MATCH_PARENT, | ||||
|     targetHeight: Int = ViewGroup.LayoutParams.WRAP_CONTENT | ||||
| ) { | ||||
|     var measuredHeight = 0 | ||||
|     performExpand( | ||||
|         duration, | ||||
|         targetWidth, | ||||
|         targetHeight, | ||||
|         { | ||||
|             measuredHeight = this.measuredHeight | ||||
|         } | ||||
|     ) { interpolatedTime, _ -> | ||||
|         layoutParams.height = if (interpolatedTime == 1f) targetHeight else (measuredHeight * interpolatedTime).toInt() | ||||
|     } | ||||
| } | ||||
|  | ||||
| @PreviewFeature | ||||
| fun View.expandHorizontally( | ||||
|     duration: Long = 500, | ||||
|     targetWidth: Int = ViewGroup.LayoutParams.MATCH_PARENT, | ||||
|     targetHeight: Int = ViewGroup.LayoutParams.WRAP_CONTENT | ||||
| ) { | ||||
|     var measuredWidth = 0 | ||||
|     performExpand( | ||||
|         duration, | ||||
|         targetWidth, | ||||
|         targetHeight, | ||||
|         { | ||||
|             measuredWidth = this.measuredWidth | ||||
|         } | ||||
|     ) { interpolatedTime, _ -> | ||||
|         layoutParams.width = if (interpolatedTime == 1f) targetWidth else (measuredWidth * interpolatedTime).toInt() | ||||
|     } | ||||
| } | ||||
|  | ||||
| @PreviewFeature | ||||
| fun View.collapse(duration: Long = 500) { | ||||
|     val initialHeight: Int = measuredHeight | ||||
|     val a: Animation = object : Animation() { | ||||
|         override fun applyTransformation(interpolatedTime: Float, t: Transformation?) { | ||||
|             if (interpolatedTime == 1f) { | ||||
|                 visibility = View.GONE | ||||
|             } else { | ||||
|                 layoutParams.height = initialHeight - (initialHeight * interpolatedTime).toInt() | ||||
|                 requestLayout() | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         override fun willChangeBounds(): Boolean { | ||||
|             return true | ||||
|         } | ||||
|     performCollapse(duration) { interpolatedTime, _ -> | ||||
|         layoutParams.height = initialHeight - (initialHeight * interpolatedTime).toInt() | ||||
|     } | ||||
| } | ||||
|  | ||||
|     a.duration = duration | ||||
|  | ||||
|     startAnimation(a) | ||||
| @PreviewFeature | ||||
| fun View.collapseHorizontally(duration: Long = 500) { | ||||
|     val initialWidth: Int = measuredWidth | ||||
|     performCollapse(duration) { interpolatedTime, _ -> | ||||
|         layoutParams.width = initialWidth - (initialWidth * interpolatedTime).toInt() | ||||
|     } | ||||
| } | ||||
|  | ||||
| @PreviewFeature | ||||
| @@ -74,3 +126,15 @@ fun View.toggleExpandState(duration: Long = 500): Boolean = if (isCollapsed) { | ||||
|     collapse(duration) | ||||
|     false | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * @return true in case of expanding | ||||
|  */ | ||||
| @PreviewFeature | ||||
| fun View.toggleExpandHorizontallyState(duration: Long = 500): Boolean = if (isCollapsed) { | ||||
|     expandHorizontally(duration) | ||||
|     true | ||||
| } else { | ||||
|     collapseHorizontally(duration) | ||||
|     false | ||||
| } | ||||
|   | ||||
| @@ -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() | ||||
|     } | ||||
| } | ||||
|  | ||||
| fun View.changeVisibility(show: Boolean = !isShown, goneOnHide: Boolean = true) { | ||||
|     if (show) { | ||||
|         show() | ||||
|     } else { | ||||
|         if (goneOnHide) { | ||||
|             gone() | ||||
|         } else { | ||||
|             hide() | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -10,13 +10,19 @@ kotlin { | ||||
|     sourceSets { | ||||
|         commonMain { | ||||
|             dependencies { | ||||
|                 api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version" | ||||
|                 api libs.kt.coroutines | ||||
|             } | ||||
|         } | ||||
|         jsMain { | ||||
|             dependencies { | ||||
|                 api project(":micro_utils.common") | ||||
|             } | ||||
|         } | ||||
|         androidMain { | ||||
|             dependencies { | ||||
|                 api "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version" | ||||
|                 api libs.kt.coroutines.android | ||||
|             } | ||||
|             dependsOn(jvmMain) | ||||
|         } | ||||
|     } | ||||
| } | ||||
| } | ||||
|   | ||||
							
								
								
									
										20
									
								
								coroutines/compose/build.gradle
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								coroutines/compose/build.gradle
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | ||||
| plugins { | ||||
|     id "org.jetbrains.kotlin.multiplatform" | ||||
|     id "org.jetbrains.kotlin.plugin.serialization" | ||||
|     id "com.android.library" | ||||
|     alias(libs.plugins.jb.compose) | ||||
| } | ||||
|  | ||||
| apply from: "$mppProjectWithSerializationAndComposePresetPath" | ||||
|  | ||||
| kotlin { | ||||
|     sourceSets { | ||||
|         commonMain { | ||||
|             dependencies { | ||||
|                 api libs.kt.coroutines | ||||
|                 api project(":micro_utils.coroutines") | ||||
|                 api project(":micro_utils.common.compose") | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,60 @@ | ||||
| package dev.inmo.micro_utils.coroutines.compose | ||||
|  | ||||
| import androidx.compose.runtime.* | ||||
| import androidx.compose.runtime.snapshots.SnapshotStateList | ||||
| import dev.inmo.micro_utils.common.applyDiff | ||||
| import dev.inmo.micro_utils.coroutines.ExceptionHandler | ||||
| import dev.inmo.micro_utils.coroutines.defaultSafelyWithoutExceptionHandlerWithNull | ||||
| import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions | ||||
| import kotlinx.coroutines.CoroutineScope | ||||
| import kotlinx.coroutines.Dispatchers | ||||
| import kotlinx.coroutines.flow.Flow | ||||
| import kotlinx.coroutines.flow.StateFlow | ||||
| import kotlinx.coroutines.withContext | ||||
| import kotlin.coroutines.CoroutineContext | ||||
|  | ||||
| /** | ||||
|  * Each value of [this] [Flow] will trigger [applyDiff] to the result [SnapshotStateList] | ||||
|  * | ||||
|  * @param scope Will be used to [subscribeSafelyWithoutExceptions] on [this] to update returned [SnapshotStateList] | ||||
|  * @param useContextOnChange Will be used to change context inside of [subscribeSafelyWithoutExceptions] to ensure that | ||||
|  * change will happen in the required [CoroutineContext]. [Dispatchers.Main] by default | ||||
|  * @param onException Will be passed to the [subscribeSafelyWithoutExceptions] as uncaught exceptions handler | ||||
|  */ | ||||
| @Suppress("NOTHING_TO_INLINE") | ||||
| inline fun <reified T> Flow<List<T>>.asMutableComposeListState( | ||||
|     scope: CoroutineScope, | ||||
|     useContextOnChange: CoroutineContext? = Dispatchers.Main, | ||||
|     noinline onException: ExceptionHandler<List<T>?> = defaultSafelyWithoutExceptionHandlerWithNull, | ||||
| ): SnapshotStateList<T> { | ||||
|     val state = mutableStateListOf<T>() | ||||
|     val changeBlock: suspend (List<T>) -> Unit = useContextOnChange ?.let { | ||||
|         { | ||||
|             withContext(useContextOnChange) { | ||||
|                 state.applyDiff(it) | ||||
|             } | ||||
|         } | ||||
|     } ?: { | ||||
|         state.applyDiff(it) | ||||
|     } | ||||
|     subscribeSafelyWithoutExceptions(scope, onException, changeBlock) | ||||
|     return state | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * In fact, it is just classcast of [asMutableComposeListState] to [List] | ||||
|  * | ||||
|  * @param scope Will be used to [subscribeSafelyWithoutExceptions] on [this] to update returned [List] | ||||
|  * @param useContextOnChange Will be used to change context inside of [subscribeSafelyWithoutExceptions] to ensure that | ||||
|  * change will happen in the required [CoroutineContext]. [Dispatchers.Main] by default | ||||
|  * @param onException Will be passed to the [subscribeSafelyWithoutExceptions] as uncaught exceptions handler | ||||
|  * | ||||
|  * @return Changing in time [List] which follow [Flow] values | ||||
|  */ | ||||
| @Suppress("NOTHING_TO_INLINE") | ||||
| inline fun <reified T> Flow<List<T>>.asComposeList( | ||||
|     scope: CoroutineScope, | ||||
|     useContextOnChange: CoroutineContext? = Dispatchers.Main, | ||||
|     noinline onException: ExceptionHandler<List<T>?> = defaultSafelyWithoutExceptionHandlerWithNull, | ||||
| ): List<T> = asMutableComposeListState(scope, useContextOnChange, onException) | ||||
|  | ||||
| @@ -0,0 +1,94 @@ | ||||
| package dev.inmo.micro_utils.coroutines.compose | ||||
|  | ||||
| import androidx.compose.runtime.* | ||||
| import dev.inmo.micro_utils.common.compose.asState | ||||
| import dev.inmo.micro_utils.coroutines.ExceptionHandler | ||||
| import dev.inmo.micro_utils.coroutines.defaultSafelyWithoutExceptionHandlerWithNull | ||||
| import dev.inmo.micro_utils.coroutines.doInUI | ||||
| import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions | ||||
| import kotlinx.coroutines.CoroutineScope | ||||
| import kotlinx.coroutines.Dispatchers | ||||
| import kotlinx.coroutines.flow.Flow | ||||
| import kotlinx.coroutines.flow.StateFlow | ||||
| import kotlinx.coroutines.withContext | ||||
| import kotlin.coroutines.CoroutineContext | ||||
|  | ||||
| /** | ||||
|  * Will map [this] [Flow] as [MutableState]. Returned [MutableState] WILL NOT change source [Flow] | ||||
|  * | ||||
|  * @param initial First value which will be passed to the result [MutableState] | ||||
|  * @param scope Will be used to [subscribeSafelyWithoutExceptions] on [this] to update returned [MutableState] | ||||
|  * @param useContextOnChange Will be used to change context inside of [subscribeSafelyWithoutExceptions] to ensure that | ||||
|  * change will happen in the required [CoroutineContext]. [Dispatchers.Main] by default | ||||
|  * @param onException Will be passed to the [subscribeSafelyWithoutExceptions] as uncaught exceptions handler | ||||
|  */ | ||||
| fun <T> Flow<T>.asMutableComposeState( | ||||
|     initial: T, | ||||
|     scope: CoroutineScope, | ||||
|     useContextOnChange: CoroutineContext? = Dispatchers.Main, | ||||
|     onException: ExceptionHandler<T?> = defaultSafelyWithoutExceptionHandlerWithNull, | ||||
| ): MutableState<T> { | ||||
|     val state = mutableStateOf(initial) | ||||
|     val changeBlock: suspend (T) -> Unit = useContextOnChange ?.let { | ||||
|         { | ||||
|             withContext(useContextOnChange) { | ||||
|                 state.value = it | ||||
|             } | ||||
|         } | ||||
|     } ?: { | ||||
|         state.value = it | ||||
|     } | ||||
|     subscribeSafelyWithoutExceptions(scope, onException, block = changeBlock) | ||||
|     return state | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Will map [this] [StateFlow] as [MutableState]. Returned [MutableState] WILL NOT change source [StateFlow]. | ||||
|  * This conversation will pass its [StateFlow.value] as the first value | ||||
|  * | ||||
|  * @param scope Will be used to [subscribeSafelyWithoutExceptions] on [this] to update returned [MutableState] | ||||
|  * @param useContextOnChange Will be used to change context inside of [subscribeSafelyWithoutExceptions] to ensure that | ||||
|  * change will happen in the required [CoroutineContext]. [Dispatchers.Main] by default | ||||
|  * @param onException Will be passed to the [subscribeSafelyWithoutExceptions] as uncaught exceptions handler | ||||
|  */ | ||||
| @Suppress("NOTHING_TO_INLINE") | ||||
| inline fun <T> StateFlow<T>.asMutableComposeState( | ||||
|     scope: CoroutineScope, | ||||
|     useContextOnChange: CoroutineContext? = Dispatchers.Main, | ||||
|     noinline onException: ExceptionHandler<T?> = defaultSafelyWithoutExceptionHandlerWithNull, | ||||
| ): MutableState<T> = asMutableComposeState(value, scope, useContextOnChange, onException) | ||||
|  | ||||
| /** | ||||
|  * Will create [MutableState] using [asMutableComposeState] and use [asState] to convert it as immutable state | ||||
|  * | ||||
|  * @param initial First value which will be passed to the result [State] | ||||
|  * @param scope Will be used to [subscribeSafelyWithoutExceptions] on [this] to update returned [State] | ||||
|  * @param useContextOnChange Will be used to change context inside of [subscribeSafelyWithoutExceptions] to ensure that | ||||
|  * change will happen in the required [CoroutineContext]. [Dispatchers.Main] by default | ||||
|  * @param onException Will be passed to the [subscribeSafelyWithoutExceptions] as uncaught exceptions handler | ||||
|  */ | ||||
| fun <T> Flow<T>.asComposeState( | ||||
|     initial: T, | ||||
|     scope: CoroutineScope, | ||||
|     useContextOnChange: CoroutineContext? = Dispatchers.Main, | ||||
|     onException: ExceptionHandler<T?> = defaultSafelyWithoutExceptionHandlerWithNull, | ||||
| ): State<T> { | ||||
|     val state = asMutableComposeState(initial, scope, useContextOnChange, onException) | ||||
|     return state.asState() | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Will map [this] [StateFlow] as [State]. This conversation will pass its [StateFlow.value] as the first value | ||||
|  * | ||||
|  * @param scope Will be used to [subscribeSafelyWithoutExceptions] on [this] to update returned [State] | ||||
|  * @param useContextOnChange Will be used to change context inside of [subscribeSafelyWithoutExceptions] to ensure that | ||||
|  * change will happen in the required [CoroutineContext]. [Dispatchers.Main] by default | ||||
|  * @param onException Will be passed to the [subscribeSafelyWithoutExceptions] as uncaught exceptions handler | ||||
|  */ | ||||
| @Suppress("NOTHING_TO_INLINE") | ||||
| inline fun <T> StateFlow<T>.asComposeState( | ||||
|     scope: CoroutineScope, | ||||
|     useContextOnChange: CoroutineContext? = Dispatchers.Main, | ||||
|     noinline onException: ExceptionHandler<T?> = defaultSafelyWithoutExceptionHandlerWithNull, | ||||
| ): State<T> = asComposeState(value, scope, useContextOnChange, onException) | ||||
|  | ||||
| @@ -0,0 +1,21 @@ | ||||
| package dev.inmo.micro_utils.coroutines.compose | ||||
|  | ||||
| import androidx.compose.runtime.MutableState | ||||
| import androidx.compose.runtime.mutableStateOf | ||||
| import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions | ||||
| import kotlinx.coroutines.CoroutineScope | ||||
| import kotlinx.coroutines.flow.Flow | ||||
| import kotlinx.coroutines.flow.StateFlow | ||||
|  | ||||
| @Deprecated("Duplicated functionality", ReplaceWith("asMutableComposeState(initial, scope)", "dev.inmo.micro_utils.coroutines.compose.asMutableComposeState")) | ||||
| fun <T> Flow<T>.toMutableState( | ||||
|     initial: T, | ||||
|     scope: CoroutineScope | ||||
| ): MutableState<T> = asMutableComposeState(initial, scope) | ||||
|  | ||||
| @Deprecated("Duplicated functionality", ReplaceWith("asMutableComposeState(scope)", "dev.inmo.micro_utils.coroutines.compose.asMutableComposeState")) | ||||
| @Suppress("NOTHING_TO_INLINE") | ||||
| inline fun <T> StateFlow<T>.toMutableState( | ||||
|     scope: CoroutineScope | ||||
| ): MutableState<T> = asMutableComposeState(scope) | ||||
|  | ||||
| @@ -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,25 +1,21 @@ | ||||
| package dev.inmo.micro_utils.coroutines | ||||
|  | ||||
| import kotlinx.coroutines.CoroutineScope | ||||
| import kotlinx.coroutines.* | ||||
| import kotlinx.coroutines.channels.Channel | ||||
| import kotlinx.coroutines.launch | ||||
| import kotlinx.coroutines.flow.consumeAsFlow | ||||
|  | ||||
| fun <T> CoroutineScope.actor( | ||||
|     channelCapacity: Int = Channel.UNLIMITED, | ||||
|     block: suspend (T) -> Unit | ||||
| ): Channel<T> { | ||||
|     val channel = Channel<T>(channelCapacity) | ||||
|     launch { | ||||
|         for (data in channel) { | ||||
|             block(data) | ||||
|         } | ||||
|     } | ||||
|     channel.consumeAsFlow().subscribe(this, block) | ||||
|     return channel | ||||
| } | ||||
|  | ||||
| inline fun <T> CoroutineScope.safeActor( | ||||
|     channelCapacity: Int = Channel.UNLIMITED, | ||||
|     noinline onException: ExceptionHandler<Unit> = {}, | ||||
|     noinline onException: ExceptionHandler<Unit> = defaultSafelyExceptionHandler, | ||||
|     crossinline block: suspend (T) -> Unit | ||||
| ): Channel<T> = actor( | ||||
|     channelCapacity | ||||
|   | ||||
| @@ -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,5 @@ | ||||
| package dev.inmo.micro_utils.coroutines | ||||
|  | ||||
| import kotlinx.coroutines.flow.FlowCollector | ||||
|  | ||||
| suspend inline operator fun <T> FlowCollector<T>.invoke(value: T) = emit(value) | ||||
| @@ -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.flow.* | ||||
| import kotlinx.coroutines.sync.Mutex | ||||
| import kotlinx.coroutines.sync.withLock | ||||
|  | ||||
| /** | ||||
|  * Shortcut for chain if [Flow.onEach] and [Flow.launchIn] | ||||
| @@ -16,7 +18,7 @@ inline fun <T> Flow<T>.subscribe(scope: CoroutineScope, noinline block: suspend | ||||
|  */ | ||||
| inline fun <T> Flow<T>.subscribeSafely( | ||||
|     scope: CoroutineScope, | ||||
|     noinline onException: ExceptionHandler<Unit> = { throw it }, | ||||
|     noinline onException: ExceptionHandler<Unit> = defaultSafelyExceptionHandler, | ||||
|     noinline block: suspend (T) -> Unit | ||||
| ) = subscribe(scope) { | ||||
|     safely(onException) { | ||||
| @@ -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( | ||||
|     scope: CoroutineScope, | ||||
|     noinline onException: ExceptionHandler<T?> = defaultSafelyWithoutExceptionHandlerWithNull, | ||||
|     noinline block: suspend (T) -> Unit | ||||
| ) = subscribeSafely( | ||||
|     scope, | ||||
|     {}, | ||||
|     block | ||||
| ) | ||||
| ) = subscribe(scope) { | ||||
|     safelyWithoutExceptions(onException) { | ||||
|         block(it) | ||||
|     } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * 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) | ||||
|     } | ||||
| } | ||||
| @@ -1,30 +1,167 @@ | ||||
| package dev.inmo.micro_utils.coroutines | ||||
|  | ||||
| import kotlinx.coroutines.CoroutineScope | ||||
| import kotlinx.coroutines.supervisorScope | ||||
|  | ||||
| import kotlinx.coroutines.* | ||||
| import kotlin.coroutines.CoroutineContext | ||||
| import kotlin.coroutines.coroutineContext | ||||
|  | ||||
| typealias ExceptionHandler<T> = suspend (Throwable) -> T | ||||
|  | ||||
| /** | ||||
|  * This instance will be used in all calls of [safely] where exception handler has not been passed | ||||
|  */ | ||||
| var defaultSafelyExceptionHandler: ExceptionHandler<Nothing> = { throw it } | ||||
|  | ||||
| /** | ||||
|  * This instance will be used in all calls of [safelyWithoutExceptions] as an exception handler for [safely] call | ||||
|  */ | ||||
| var defaultSafelyWithoutExceptionHandler: ExceptionHandler<Unit> = { | ||||
|     try { | ||||
|         defaultSafelyExceptionHandler(it) | ||||
|     } catch (e: Throwable) { | ||||
|          // do nothing | ||||
|     } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * This key can (and will) be used to get [ContextSafelyExceptionHandler] from [coroutineContext] of suspend functions | ||||
|  * and in [ContextSafelyExceptionHandler] for defining of its [CoroutineContext.Element.key] | ||||
|  * | ||||
|  * @see safelyWithContextExceptionHandler | ||||
|  * @see ContextSafelyExceptionHandler | ||||
|  */ | ||||
| object ContextSafelyExceptionHandlerKey : CoroutineContext.Key<ContextSafelyExceptionHandler> | ||||
|  | ||||
| /** | ||||
|  * [ExceptionHandler] wrapper which was created to make possible to use [handler] across all coroutines calls | ||||
|  * | ||||
|  * @see safelyWithContextExceptionHandler | ||||
|  * @see ContextSafelyExceptionHandlerKey | ||||
|  */ | ||||
| class ContextSafelyExceptionHandler( | ||||
|     val handler: ExceptionHandler<Unit> | ||||
| ) : CoroutineContext.Element { | ||||
|     override val key: CoroutineContext.Key<*> | ||||
|         get() = ContextSafelyExceptionHandlerKey | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * @return [ContextSafelyExceptionHandler] from [coroutineContext] by key [ContextSafelyExceptionHandlerKey] if | ||||
|  * exists | ||||
|  * | ||||
|  * @see ContextSafelyExceptionHandler | ||||
|  * @see ContextSafelyExceptionHandlerKey | ||||
|  */ | ||||
| suspend inline fun contextSafelyExceptionHandler() = coroutineContext[ContextSafelyExceptionHandlerKey] | ||||
|  | ||||
| /** | ||||
|  * This method will set new [coroutineContext] with [ContextSafelyExceptionHandler]. In case if [coroutineContext] | ||||
|  * already contains [ContextSafelyExceptionHandler], [ContextSafelyExceptionHandler.handler] will be used BEFORE | ||||
|  * [contextExceptionHandler] in case of exception. | ||||
|  * | ||||
|  * After all, will be called [withContext] method with created [ContextSafelyExceptionHandler] and block which will call | ||||
|  * [safely] method with [safelyExceptionHandler] as onException parameter and [block] as execution block | ||||
|  */ | ||||
| suspend fun <T> safelyWithContextExceptionHandler( | ||||
|     contextExceptionHandler: ExceptionHandler<Unit>, | ||||
|     safelyExceptionHandler: ExceptionHandler<T> = defaultSafelyExceptionHandler, | ||||
|     block: suspend CoroutineScope.() -> T | ||||
| ): T { | ||||
|     val contextSafelyExceptionHandler = contextSafelyExceptionHandler() ?.handler ?.let { oldHandler -> | ||||
|         ContextSafelyExceptionHandler { | ||||
|             oldHandler(it) | ||||
|             contextExceptionHandler(it) | ||||
|         } | ||||
|     } ?: ContextSafelyExceptionHandler(contextExceptionHandler) | ||||
|     return withContext(contextSafelyExceptionHandler) { | ||||
|         safely(safelyExceptionHandler, block) | ||||
|     } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * It will run [block] inside of [supervisorScope] to avoid problems with catching of exceptions | ||||
|  * | ||||
|  * Priorities of [ExceptionHandler]s: | ||||
|  * | ||||
|  * * [onException] In case if custom (will be used anyway if not [defaultSafelyExceptionHandler]) | ||||
|  * * [CoroutineContext.get] with [SafelyExceptionHandlerKey] as key | ||||
|  * * [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 | ||||
|  * exception will be available for catching | ||||
|  * | ||||
|  * @see defaultSafelyExceptionHandler | ||||
|  * @see safelyWithoutExceptions | ||||
|  * @see safelyWithContextExceptionHandler | ||||
|  */ | ||||
| suspend inline fun <T> safely( | ||||
|     noinline onException: ExceptionHandler<T> = { throw it }, | ||||
|     noinline onException: ExceptionHandler<T> = defaultSafelyExceptionHandler, | ||||
|     noinline block: suspend CoroutineScope.() -> T | ||||
| ): T { | ||||
|     return try { | ||||
|         supervisorScope(block) | ||||
|     } catch (e: Throwable) { | ||||
|         coroutineContext[ContextSafelyExceptionHandlerKey] ?.handler ?.invoke(e) | ||||
|         onException(e) | ||||
|     } | ||||
| } | ||||
|  | ||||
| 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( | ||||
|     noinline onException: ExceptionHandler<T?> = defaultSafelyWithoutExceptionHandlerWithNull, | ||||
|     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,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,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) | ||||
| } | ||||
| @@ -0,0 +1,26 @@ | ||||
| package dev.inmo.micro_utils.coroutines | ||||
|  | ||||
| import kotlinx.coroutines.* | ||||
|  | ||||
| fun <T> CoroutineScope.launchSynchronously(block: suspend CoroutineScope.() -> T): T { | ||||
|     var result: Result<T>? = null | ||||
|     val objectToSynchronize = Object() | ||||
|     synchronized(objectToSynchronize) { | ||||
|         launch(start = CoroutineStart.UNDISPATCHED) { | ||||
|             result = safelyWithResult(block) | ||||
|         }.invokeOnCompletion { | ||||
|             synchronized(objectToSynchronize) { | ||||
|                 objectToSynchronize.notifyAll() | ||||
|             } | ||||
|         } | ||||
|         while (result == null) { | ||||
|             objectToSynchronize.wait() | ||||
|         } | ||||
|     } | ||||
|     return result!!.getOrThrow() | ||||
| } | ||||
|  | ||||
| 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,38 @@ | ||||
| package dev.inmo.micro_utils.coroutines | ||||
|  | ||||
| import kotlinx.coroutines.* | ||||
| import kotlin.test.Test | ||||
|  | ||||
| class HandleSafelyCoroutineContextTest { | ||||
|     @Test | ||||
|     fun testHandleSafelyCoroutineContext() { | ||||
|         val scope = CoroutineScope(Dispatchers.Default) | ||||
|         var contextHandlerHappen = false | ||||
|         var localHandlerHappen = false | ||||
|         var defaultHandlerHappen = false | ||||
|         defaultSafelyExceptionHandler = { | ||||
|             defaultHandlerHappen = true | ||||
|             throw it | ||||
|         } | ||||
|         val contextHandler: ExceptionHandler<Unit> = { | ||||
|             contextHandlerHappen = true | ||||
|         } | ||||
|         val checkJob = scope.launch { | ||||
|             safelyWithContextExceptionHandler(contextHandler) { | ||||
|                 safely( | ||||
|                     { | ||||
|                         localHandlerHappen = true | ||||
|                     } | ||||
|                 ) { | ||||
|                     error("That must happen :)") | ||||
|                 } | ||||
|                 println(coroutineContext) | ||||
|                 error("That must happen too:)") | ||||
|             } | ||||
|         } | ||||
|         launchSynchronously { checkJob.join() } | ||||
|         assert(contextHandlerHappen) | ||||
|         assert(localHandlerHappen) | ||||
|         assert(defaultHandlerHappen) | ||||
|     } | ||||
| } | ||||
| @@ -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,13 @@ | ||||
| package dev.inmo.micro_utils.coroutines | ||||
|  | ||||
| import kotlin.test.Test | ||||
| import kotlin.test.assertEquals | ||||
|  | ||||
| class LaunchSynchronouslyTest { | ||||
|     @Test | ||||
|     fun testRunInCoroutine() { | ||||
|         (0 .. 10000).forEach { | ||||
|             assertEquals(it, launchSynchronously { it }) | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,40 @@ | ||||
| package dev.inmo.micro_utils.coroutines | ||||
|  | ||||
| import kotlinx.coroutines.* | ||||
| import org.junit.Test | ||||
|  | ||||
| class WeakJob { | ||||
|     @Test | ||||
|     fun `test that weak jobs works correctly`() { | ||||
|         val scope = CoroutineScope(Dispatchers.Default) | ||||
|         lateinit var weakLaunchJob: Job | ||||
|         lateinit var weakAsyncJob: Job | ||||
|         scope.launchSynchronously { | ||||
|             val completeDeferred = Job() | ||||
|             coroutineScope { | ||||
|                 weakLaunchJob = weakLaunch { | ||||
|                     while (isActive) { | ||||
|                         delay(100L) | ||||
|                     } | ||||
|                 } | ||||
|                 weakAsyncJob = weakAsync { | ||||
|                     while (isActive) { | ||||
|                         delay(100L) | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 coroutineContext.job.invokeOnCompletion { | ||||
|                     scope.launch { | ||||
|                         delay(1000L) | ||||
|                         completeDeferred.complete() | ||||
|                     } | ||||
|                 } | ||||
|                 launch { delay(1000L); cancel() } | ||||
|             } | ||||
|             completeDeferred.join() | ||||
|         } | ||||
|  | ||||
|         assert(!weakLaunchJob.isActive) | ||||
|         assert(!weakAsyncJob.isActive) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										50
									
								
								coroutines/src/main/kotlin/FlowOnHierarchyChangeListener.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								coroutines/src/main/kotlin/FlowOnHierarchyChangeListener.kt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,50 @@ | ||||
| package dev.inmo.micro_utils.coroutines | ||||
|  | ||||
| import android.view.View | ||||
| import android.view.ViewGroup | ||||
| import kotlinx.coroutines.flow.MutableSharedFlow | ||||
| import kotlinx.coroutines.flow.asSharedFlow | ||||
|  | ||||
| /** | ||||
|  * [kotlinx.coroutines.flow.Flow]-based [android.view.ViewGroup.OnHierarchyChangeListener] | ||||
|  * | ||||
|  * @param recursive If set, any call of [onChildViewAdded] will check if child [View] is [ViewGroup] and subscribe to this | ||||
|  * [ViewGroup] too | ||||
|  * @param [_onChildViewAdded] Internal [MutableSharedFlow] which will be used to pass data to [onChildViewAdded] flow | ||||
|  * @param [_onChildViewRemoved] Internal [MutableSharedFlow] which will be used to pass data to [onChildViewRemoved] flow | ||||
|  */ | ||||
| class FlowOnHierarchyChangeListener( | ||||
|     private val recursive: Boolean = false, | ||||
|     private val _onChildViewAdded: MutableSharedFlow<Pair<View, View>> = MutableSharedFlow(extraBufferCapacity = Int.MAX_VALUE), | ||||
|     private val _onChildViewRemoved: MutableSharedFlow<Pair<View, View>> = MutableSharedFlow(extraBufferCapacity = Int.MAX_VALUE) | ||||
| ) : ViewGroup.OnHierarchyChangeListener { | ||||
|     val onChildViewAdded = _onChildViewAdded.asSharedFlow() | ||||
|     val onChildViewRemoved = _onChildViewRemoved.asSharedFlow() | ||||
|  | ||||
|     /** | ||||
|      * Will emit data into [onChildViewAdded] flow. If [recursive] is true and [child] is [ViewGroup] will also | ||||
|      * subscribe to [child] hierarchy changes. | ||||
|      * | ||||
|      * Due to the fact that this method is not suspendable, [FlowOnHierarchyChangeListener] will use | ||||
|      * [MutableSharedFlow.tryEmit] to send data into [_onChildViewAdded]. That is why its default extraBufferCapacity is | ||||
|      * [Int.MAX_VALUE] | ||||
|      */ | ||||
|     override fun onChildViewAdded(parent: View, child: View) { | ||||
|         _onChildViewAdded.tryEmit(parent to child) | ||||
|  | ||||
|         if (recursive && child is ViewGroup) { | ||||
|             child.setOnHierarchyChangeListener(this) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Just emit data into [onChildViewRemoved] | ||||
|      * | ||||
|      * Due to the fact that this method is not suspendable, [FlowOnHierarchyChangeListener] will use | ||||
|      * [MutableSharedFlow.tryEmit] to send data into [_onChildViewRemoved]. That is why its default extraBufferCapacity is | ||||
|      * [Int.MAX_VALUE] | ||||
|      */ | ||||
|     override fun onChildViewRemoved(parent: View, child: View) { | ||||
|         _onChildViewRemoved.tryEmit(parent to child) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										17
									
								
								coroutines/src/main/kotlin/RecursiveHierarchySubscriber.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								coroutines/src/main/kotlin/RecursiveHierarchySubscriber.kt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| package dev.inmo.micro_utils.coroutines | ||||
|  | ||||
| import android.view.ViewGroup | ||||
| import android.view.ViewGroup.OnHierarchyChangeListener | ||||
|  | ||||
| /** | ||||
|  * Use [ViewGroup.setOnHierarchyChangeListener] recursively for all available [ViewGroup]s starting with [this]. | ||||
|  * This extension DO NOT guarantee that recursive subscription will happen after this method call | ||||
|  */ | ||||
| fun ViewGroup.setOnHierarchyChangeListenerRecursively( | ||||
|     listener: OnHierarchyChangeListener | ||||
| ) { | ||||
|     setOnHierarchyChangeListener(listener) | ||||
|     (0 until childCount).forEach { | ||||
|         (getChildAt(it) as? ViewGroup) ?.setOnHierarchyChangeListenerRecursively(listener) | ||||
|     } | ||||
| } | ||||
| @@ -1,10 +0,0 @@ | ||||
| package dev.inmo.micro_utils.coroutines | ||||
|  | ||||
| import kotlinx.coroutines.CoroutineScope | ||||
| import kotlinx.coroutines.Dispatchers | ||||
| import kotlinx.coroutines.withContext | ||||
|  | ||||
| suspend inline fun <T> doInUI(noinline block: suspend CoroutineScope.() -> T) = withContext( | ||||
|     Dispatchers.Main, | ||||
|     block | ||||
| ) | ||||
							
								
								
									
										30
									
								
								crypto/build.gradle
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								crypto/build.gradle
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | ||||
| plugins { | ||||
|     id "org.jetbrains.kotlin.multiplatform" | ||||
|     id "org.jetbrains.kotlin.plugin.serialization" | ||||
|     id "com.android.library" | ||||
| } | ||||
|  | ||||
| apply from: "$mppProjectWithSerializationPresetPath" | ||||
|  | ||||
| kotlin { | ||||
|     sourceSets { | ||||
|         commonMain { | ||||
|             dependencies { | ||||
|                 api project(":micro_utils.common") | ||||
|             } | ||||
|         } | ||||
|         jsMain { | ||||
|             dependencies { | ||||
|                 api npm("crypto-js", "$crypto_js_version") | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| android { | ||||
|     sourceSets { | ||||
|         main { | ||||
|             java.srcDirs += [ "src/jvmMain/kotlin" ] | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,77 @@ | ||||
| package dev.inmo.micro_utils.crypto | ||||
|  | ||||
| import kotlin.experimental.and | ||||
|  | ||||
| private const val BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" | ||||
| private const val BASE64_MASK: Byte = 0x3f | ||||
| private const val BASE64_PAD = '=' | ||||
| private val BASE64_INVERSE_ALPHABET = IntArray(256) { | ||||
|     BASE64_ALPHABET.indexOf(it.toChar()) | ||||
| } | ||||
|  | ||||
| internal fun Int.toBase64(): Char = BASE64_ALPHABET[this] | ||||
| internal fun Byte.fromBase64(): Byte = BASE64_INVERSE_ALPHABET[toInt() and 0xff].toByte() and BASE64_MASK | ||||
|  | ||||
| typealias EncodedBase64String = String | ||||
| typealias EncodedByteArray = ByteArray | ||||
|  | ||||
| fun SourceString.encodeBase64String(): EncodedBase64String = encodeToByteArray().encodeBase64String() | ||||
| fun SourceString.encodeBase64(): EncodedByteArray = encodeToByteArray().encodeBase64() | ||||
|  | ||||
| fun SourceBytes.encodeBase64String(): EncodedBase64String = buildString { | ||||
|     var i = 0 | ||||
|     while (this@encodeBase64String.size > i) { | ||||
|         val read = kotlin.math.min(3, this@encodeBase64String.size - i) | ||||
|         val data = ByteArray(3) { | ||||
|             if (it < read) { | ||||
|                 this@encodeBase64String[it + i] | ||||
|             } else { | ||||
|                 0 | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         val padSize = (data.size - read) * 8 / 6 | ||||
|         val chunk = ((data[0].toInt() and 0xFF) shl 16) or | ||||
|             ((data[1].toInt() and 0xFF) shl 8) or | ||||
|             (data[2].toInt() and 0xFF) | ||||
|  | ||||
|         for (index in data.size downTo padSize) { | ||||
|             val char = (chunk shr (6 * index)) and BASE64_MASK.toInt() | ||||
|             append(char.toBase64()) | ||||
|         } | ||||
|  | ||||
|         repeat(padSize) { append(BASE64_PAD) } | ||||
|  | ||||
|         i += read | ||||
|     } | ||||
| } | ||||
| fun SourceBytes.encodeBase64(): EncodedByteArray = encodeBase64String().encodeToByteArray() | ||||
|  | ||||
| fun EncodedBase64String.decodeBase64(): SourceBytes = dropLastWhile { it == BASE64_PAD }.encodeToByteArray().decodeBase64() | ||||
| fun EncodedBase64String.decodeBase64String(): SourceString = decodeBase64().decodeToString() | ||||
|  | ||||
| fun EncodedByteArray.decodeBase64(): SourceBytes { | ||||
|     val result = mutableListOf<Byte>() | ||||
|     val data = ByteArray(4) | ||||
|     (0 until size step 4).forEach { i -> | ||||
|         var read = 0 | ||||
|         for (j in 0 until 4) { | ||||
|             if (j + i < size) { | ||||
|                 data[j] = get(j + i) | ||||
|                 read++ | ||||
|             } else { | ||||
|                 break | ||||
|             } | ||||
|         } | ||||
|         val chunk = data.foldIndexed(0) { index, result, current -> | ||||
|             result or (current.fromBase64().toInt() shl ((3 - index) * 6)) | ||||
|         } | ||||
|         for (index in data.size - 2 downTo (data.size - read)) { | ||||
|             val origin = (chunk shr (8 * index)) and 0xff | ||||
|             result.add(origin.toByte()) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     return result.toByteArray() | ||||
| } | ||||
| fun EncodedByteArray.decodeBase64String(): SourceString = decodeBase64().decodeToString() | ||||
| @@ -0,0 +1,11 @@ | ||||
| package dev.inmo.micro_utils.crypto | ||||
|  | ||||
| /** | ||||
|  * This typealias shows that corresponding [ByteArray] is an raw data | ||||
|  */ | ||||
| typealias SourceBytes = ByteArray | ||||
| /** | ||||
|  * This typealias shows that corresponding [String] is an raw data | ||||
|  */ | ||||
| typealias SourceString = String | ||||
|  | ||||
| @@ -0,0 +1,15 @@ | ||||
| package dev.inmo.micro_utils.crypto | ||||
|  | ||||
| val HEX_ARRAY = "0123456789abcdef".toCharArray() | ||||
|  | ||||
| fun SourceBytes.hex(): String { | ||||
|     val hexChars = CharArray(size * 2) | ||||
|     for (j in indices) { | ||||
|         val v: Int = this[j].toInt() and 0xFF | ||||
|         hexChars[j * 2] = HEX_ARRAY[v ushr 4] | ||||
|         hexChars[j * 2 + 1] = HEX_ARRAY[v and 0x0F] | ||||
|     } | ||||
|     return hexChars.concatToString() | ||||
| } | ||||
|  | ||||
| fun SourceString.hex(): String = encodeToByteArray().hex() | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user