mirror of
				https://github.com/InsanusMokrassar/MicroUtils.git
				synced 2025-10-30 03:30:19 +00:00 
			
		
		
		
	Compare commits
	
		
			1765 Commits
		
	
	
		
			0.2.8
			...
			renovate/k
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 5b7f31de22 | ||
| 9dad353957 | |||
| 89e16b7bdb | |||
| c2965da341 | |||
| ffb072dc5f | |||
| a247dbcb02 | |||
| 1dd71175f4 | |||
| bbe62c0e7b | |||
| 9822ff321b | |||
| b485d485ef | |||
| 0b3d445109 | |||
| d7e48940bc | |||
| 1049eb0fe7 | |||
| c871ef5635 | |||
| 7edfcb20c4 | |||
| 7a1438a2c0 | |||
| 2af8cba8cd | |||
| 27d74c0a62 | |||
| f86d1bfe06 | |||
| 7cc5972ff7 | |||
| 3bbf978b00 | |||
| ed36467600 | |||
| dd0de327fc | |||
| dccd3ce8fd | |||
| fa45e7b696 | |||
| 57f009e8aa | |||
| 04b633a5ea | |||
| 20d42b05bb | |||
| 91ba50f1ff | |||
| f4476c99f9 | |||
| 50f3f586ab | |||
| 36a2d7ec8e | |||
| 4890b5833e | |||
| e20ab89688 | |||
| e557ba8184 | |||
| 8540e21d5a | |||
| 76c04a8506 | |||
| 128632770e | |||
| 31e0800e81 | |||
| 00ca96eec8 | |||
| 077ef2c639 | |||
| e3ea7be0e7 | |||
| 05fd1c2b14 | |||
| affcffe270 | |||
| 62930231e4 | |||
| ad651631ec | |||
| cf1c8f13db | |||
| 9acc69b897 | |||
| 9bc7cbdb50 | |||
| 2ed8443e28 | |||
| 94f598c2b4 | |||
| d83d30af06 | |||
| 284e763f0d | |||
| 3bfa172533 | |||
| b5b1fd6d5f | |||
| 05e0d9b7d2 | |||
| 1ae1f8dee2 | |||
| 1bf479a0b7 | |||
| 9d04d49628 | |||
| 3de324519b | |||
| 4be90d0ea5 | |||
| 041f35ed6c | |||
| 9c463d0338 | |||
| 5ec0a46089 | |||
| 979d0ee4ca | |||
| 448686b399 | |||
|  | 0ff149a3d3 | ||
| 50a90d6e96 | |||
| 5f93706d91 | |||
| 39415550f5 | |||
|  | 4586ad65b5 | ||
| 8c5678e26f | |||
| 5dff373d5f | |||
| e17868a085 | |||
| 97bb6d0936 | |||
|  | 54576d8dec | ||
| 282bb24c71 | |||
| b1a96b6ecb | |||
| 66dac2086c | |||
| e269d0d206 | |||
| 5d95c3eb9c | |||
| 26650e9b6c | |||
| 7339dd8354 | |||
| 8ae983971a | |||
| d46cc3b09c | |||
| dfd23f8d60 | |||
| 28eb1a11e6 | |||
| 04f82a03bf | |||
| aac545074b | |||
| 87a3a925ee | |||
| 5447bf9691 | |||
| 761070b9b7 | |||
| 4c9e435df8 | |||
| 4b7d65e8b4 | |||
| 0515b49b98 | |||
| edb97215ef | |||
| 5577a24548 | |||
| cfaa2a8927 | |||
| 78494b6036 | |||
| f9ea7eca61 | |||
| d69fee1732 | |||
| 178518db5e | |||
| 6fb20fb973 | |||
| 831bf44e34 | |||
| a4c6c367e3 | |||
| bc98e59709 | |||
| ef287bc331 | |||
| 3437f4c712 | |||
| 618f2dcd79 | |||
| 6df8ad3095 | |||
| eda6221288 | |||
| a9859f6a0d | |||
| 0db88bac25 | |||
| daa3d9c0dd | |||
| b343b33594 | |||
| 46e435a448 | |||
| 7fe62b4ffa | |||
| 9c94348a15 | |||
| bac256e93e | |||
| 49f59aa129 | |||
| 800dab5be0 | |||
| b9977527b2 | |||
| c216dba69d | |||
| d4148d52e3 | |||
| 2006a8cdd0 | |||
| feb52ecbd1 | |||
| 42909c3b7a | |||
| 706a787163 | |||
| 1bc14bded6 | |||
| f00cb81db1 | |||
| 2fbd14956d | |||
| 24657b43be | |||
| 659d3b6fa5 | |||
| 339483c8a3 | |||
| 51ec46bbd7 | |||
| 8a059cc26d | |||
| ddb8e1efb4 | |||
| 3bf2ed5168 | |||
| e70d34d91a | |||
| 7a650f5c2f | |||
| fc6f5ae2ee | |||
| 3b7dde3cb1 | |||
| 0a5cfaba18 | |||
| ea527b5e91 | |||
| 85f11439e8 | |||
| 98c7b48625 | |||
| 8b007bb3af | |||
| 89e2d88d1c | |||
| 260399e965 | |||
| de72843b8e | |||
| b589142d9f | |||
| f479c85869 | |||
| 26992c039a | |||
| ef50e1a24f | |||
| 2f201670d2 | |||
| 90c80573a0 | |||
| 059519cdca | |||
| 55647e2e2f | |||
| 1802be68ef | |||
| aa79496a36 | |||
| 4bfa4c32aa | |||
| d8ca29eab1 | |||
| f7a9f2e13d | |||
| af286d3d53 | |||
| 9a20e8155e | |||
| b81af7e1c8 | |||
| 74563bbac9 | |||
| 07ad79f0b3 | |||
| 5c28255e06 | |||
| 99783f281d | |||
| a5008c3d15 | |||
| f807f2beeb | |||
| db34b25ef2 | |||
| 96daf11fd4 | |||
| f7e98dfd2d | |||
| 61277e92bd | |||
| 32ef9f399f | |||
| 54e6ca5dc3 | |||
| a8e226786d | |||
| ce717a4c9f | |||
| fd41bf0ae7 | |||
| b2b68bf29f | |||
| b87c29c354 | |||
| 24977822c9 | |||
| 647daa8627 | |||
| a372efacb1 | |||
| f40d33db2a | |||
| a5eb0fbd24 | |||
| 307c8030af | |||
| a9016465fa | |||
| 358b70eb5f | |||
| 6fcbb80a71 | |||
|  | 643f6c420b | ||
| ac68b0b941 | |||
| d62f67bd88 | |||
| 8718c5e310 | |||
| e8273ab80c | |||
| 2718605987 | |||
| d99538d80b | |||
| ce7a1e4e21 | |||
| 921734763d | |||
| 18c608f569 | |||
| b915f6ece2 | |||
| 24347b422c | |||
| e5f4ae647f | |||
| 72202b8a21 | |||
| dbc14d41de | |||
| 1dca5ea00d | |||
| f03d1d788c | |||
| fcac6f9fa8 | |||
| ca0cd433c9 | |||
| 39589fdbd0 | |||
| 605fc3cff9 | |||
| 12cd6f48f8 | |||
| 1f57478d10 | |||
| 7ef4c5d282 | |||
| f09d92be32 | |||
| 6245b36bdb | |||
| 54cc353bcc | |||
| b7abba099c | |||
| c5dbd10335 | |||
| a44e3e953d | |||
| ee2521cb01 | |||
| 4625dfb857 | |||
| b9a2653066 | |||
| fcaa327660 | |||
| 496117d517 | |||
| 8ce7d37b72 | |||
| 25391609b9 | |||
| f0b7b9c5e5 | |||
| 301cdaa2c2 | |||
| fce7ec8912 | |||
| 24bd403549 | |||
| 46c89c48a9 | |||
| bad9a53fdb | |||
| 0bce7bd60a | |||
| 2f70a1cfb4 | |||
| bfb6e738ee | |||
| c7ad9aae07 | |||
| fecd719239 | |||
| 18d6ac31b5 | |||
| d8dbebfc7e | |||
| 16463d0eb9 | |||
| 837cac644d | |||
| e83e0a8535 | |||
| 2e309c31a6 | |||
| 625db02651 | |||
| e3144c08c1 | |||
| 40e25970ad | |||
| 4ccb8a8d11 | |||
| 26d1c3d9ef | |||
| cc8fe7b001 | |||
| 4601eab864 | |||
| 1ee9c88ffd | |||
| 7feac213f6 | |||
| c75a9c0f61 | |||
| afd0c9784f | |||
| f1ddbea06e | |||
| 42bf3cd4e3 | |||
| 27f5549f56 | |||
| b0569f8421 | |||
| 8d86f29325 | |||
| aeca32498a | |||
| e8232664f3 | |||
| efa869f91a | |||
| dbff3d7cf1 | |||
| 9e70f73684 | |||
| 9f2f0de0c4 | |||
| bf293a8f8f | |||
| 5fbc1a132f | |||
| 36093b9741 | |||
| 8713fb04c5 | |||
| ea82a59f31 | |||
| 18f67cd4f2 | |||
| 120e7228c7 | |||
| 892fa90c37 | |||
| 491dac5bf0 | |||
| 2ab06fbafd | |||
| e68735d061 | |||
| 9ba4d98c30 | |||
| 19bbfd4916 | |||
| d94cd7ea94 | |||
| 0dd8f41eb0 | |||
| f1ab5ab51f | |||
| a8056f3120 | |||
| 09c0cdebb5 | |||
| 629d7c7a82 | |||
| a0dd1aec3d | |||
| 17d6377902 | |||
| ad401105a1 | |||
| 4913e99c2e | |||
| 16b97029cf | |||
| 481130c9bb | |||
| 698ed6718d | |||
| d164813bb4 | |||
| 52157ee0e7 | |||
| 877b62fe5d | |||
| d823a02971 | |||
| e950056e3b | |||
| 70014ba233 | |||
| 4425f24a20 | |||
| 410964a44b | |||
| 30389e8536 | |||
| 5314833041 | |||
| 40f7cf7678 | |||
| 83a0b07062 | |||
| 1b4900d691 | |||
| f9795d53a0 | |||
| 2b9bb4f141 | |||
| 9196e4c367 | |||
| 374a5a1a37 | |||
| 827cf32c1b | |||
| 98ad6dbeb2 | |||
| 63c8f642ec | |||
| 3bfe64f797 | |||
| ec98029467 | |||
| ab58478686 | |||
| 90247667d1 | |||
| e661185534 | |||
| d73e4e8e1f | |||
| a6905e73cb | |||
| 93b054d55e | |||
| 5db4c5c717 | |||
| 5e04521929 | |||
| 30440b4ed1 | |||
| 09bb90604d | |||
| 4d55ec6f36 | |||
| f373524f34 | |||
| 0398a7bebd | |||
| fa8a5bcd97 | |||
| 758a92410b | |||
| 77f56c5dda | |||
| 72f2fe3cc3 | |||
| 7e0e520f03 | |||
| 92c4784e42 | |||
| a634229dc0 | |||
| de3f36ef2a | |||
| c623a265ee | |||
| e75125f6df | |||
| 4901a8844c | |||
| a1854b68d8 | |||
| aee4a6243b | |||
| 4c0cb73d69 | |||
| c6eab182f6 | |||
| 3e3fbd97eb | |||
| feb695caa7 | |||
| 1bd46d9651 | |||
| 32eabb6b36 | |||
| 9bfe6dc6d8 | |||
| 3f366aeea4 | |||
| 4338fd46f2 | |||
| 36974f5b49 | |||
| d48f767408 | |||
| bd2558e852 | |||
| 9f7c963cd5 | |||
| 2db0eadbfe | |||
| 580c7b8842 | |||
| 88fba347ea | |||
| c8c5255e62 | |||
| 01a1a2760a | |||
| 6d34df8d48 | |||
| b124ea65e1 | |||
| f3214347a9 | |||
| 6b8724b59f | |||
| ac3e378edf | |||
| 5d31fd1c91 | |||
| 046fe1ec08 | |||
| 721873c843 | |||
| f6ffbfc10a | |||
| 4e91649e0a | |||
| 4f7f8abec4 | |||
| 6370562dbc | |||
| 94e2e67522 | |||
| 1d8330015d | |||
| e5017b0258 | |||
| cd412ca31b | |||
| d2b6473095 | |||
| ab42507275 | |||
| dcef844e20 | |||
| 3244708c1b | |||
| 73ac1f1741 | |||
| f104e9f352 | |||
| 393c9a7d06 | |||
| ea497ea488 | |||
|  | 5add89cad0 | ||
| 2db447d2ef | |||
| 31c83813e6 | |||
| 27483a282d | |||
| 48b816aa22 | |||
| 0065f94f52 | |||
| ccc0002eb2 | |||
| 15a2eee141 | |||
| b9faac71e5 | |||
| 0a4465de33 | |||
| f9dfd09628 | |||
| 8638d7afce | |||
| 8311793a43 | |||
| 0d552cfcd2 | |||
| 4b0f20dbd1 | |||
| cf531c949d | |||
| ba5c5f17d5 | |||
| 35825ad9b7 | |||
| b1eb26a89e | |||
| c9d04b6698 | |||
| 496133e014 | |||
| f2857ee2be | |||
| 22541f2d5e | |||
| e235c52b6c | |||
| e89b8c72dd | |||
| a374e53a3a | |||
| afb066c4ee | |||
| f05761d4a5 | |||
| 76adc9ea33 | |||
| 99dd291413 | |||
| f904eb27e1 | |||
| eeb8214812 | |||
| f7215b039e | |||
| c07fe5a0f9 | |||
| 0d28cb6e20 | |||
| a1a17bfd1f | |||
| f386f09592 | |||
| a47e17fe6e | |||
| 01dc3b63ff | |||
| 2d97e0699e | |||
| 75f514d99b | |||
| 9a687cfc1c | |||
| 14edf8b6b7 | |||
| 23aa2d8917 | |||
| 7651388b5c | |||
| 805ab32b24 | |||
| cc623b1097 | |||
| 1420416b3e | |||
| 9a0b67f938 | |||
| 303e1e6281 | |||
| ff59b0cc9c | |||
| be5d2ee715 | |||
| 8dd2e3f6f9 | |||
| 2eedd196d2 | |||
| 759a3f2784 | |||
| 386fa830c3 | |||
| c382423d77 | |||
| bb466ce66c | |||
| 72cd3dd8a1 | |||
|  | 595cedaaf1 | ||
| eeaceb6cf5 | |||
| 1bd671685b | |||
| 48d3fe41f2 | |||
| 7ab21871cd | |||
| 65d01b1fb3 | |||
| 6230accb68 | |||
| c6ed821934 | |||
| 10e03bb951 | |||
| aa4f392948 | |||
| f51b59ec02 | |||
| 8c76834ae4 | |||
| 4a454f3d67 | |||
| 151aa1863d | |||
| 3bf6896296 | |||
| 0d01561476 | |||
| f6ded92251 | |||
| d01b735cc6 | |||
| 6c12001080 | |||
| 1afbf03606 | |||
| f6ef5c61c5 | |||
| c18fee8107 | |||
| d9df7a4384 | |||
| 87c2230e8e | |||
| da7eb6de0a | |||
| ed3815118f | |||
| be726f42bd | |||
| a91006132f | |||
| 9a9f741a0b | |||
| 5028f130e9 | |||
| 77fa019651 | |||
| 9715da9384 | |||
| f6d5035c1a | |||
| 43e782ab6f | |||
| f3f9920bfb | |||
| 2bfd615812 | |||
| ebfacb3659 | |||
| c71d557eec | |||
| e0398cef21 | |||
| f91599e9c6 | |||
| f8f9f93c97 | |||
| a8a5340d8b | |||
| 871b27f37d | |||
| 6f174cae1d | |||
| 22d7ac3e22 | |||
| 9b30c3a155 | |||
| 915bac64b1 | |||
| 9d2b50e55d | |||
| bde100f63d | |||
| 05b035a13d | |||
| eefb56bed7 | |||
| fcc0dc4189 | |||
| 47ff20317f | |||
| 1558b9103d | |||
| 7a78742162 | |||
| c01e240f66 | |||
| fef4fcbac6 | |||
| 5ab18bce4b | |||
| 24aec7271a | |||
| 9b19a2cb95 | |||
| efdd7b8a57 | |||
| 6df4cc9c3b | |||
| b9d93db0f5 | |||
| d7ee45ca64 | |||
| d7c31b1b22 | |||
| 7d6794a358 | |||
| 473eb87346 | |||
| 8b18b07790 | |||
| ab3c80a5ec | |||
| 075b93ecd6 | |||
| f6d0f72e49 | |||
| fcda3af862 | |||
| d5c7a589b1 | |||
| 86e099ed25 | |||
| ebd7befe73 | |||
| b8c7e581a1 | |||
| 8281259179 | |||
| d3e06b07df | |||
| 4967018418 | |||
| 537a3c38fa | |||
| 0124957833 | |||
| f0420e2d61 | |||
| 7090566041 | |||
| f0d5035cd0 | |||
| 0fb9b8dc30 | |||
| eef6e81134 | |||
| 1593159a3f | |||
| 3da9eb9dbe | |||
| f17613f3fb | |||
| 14337ccb46 | |||
| 1a3913b09c | |||
| 039aed2747 | |||
| 173991e3cb | |||
| 8b3f8cab01 | |||
| 2a20d24589 | |||
| 53c2d552ec | |||
| af11c1a83d | |||
| a65cf1481c | |||
| 0318716236 | |||
| 88eb4b3342 | |||
| 4810d1ef6a | |||
| f2a9514d89 | |||
| 925ba6ac24 | |||
|  | ef407268a2 | ||
| cd6c4bbe38 | |||
| c058e18408 | |||
| 6d3ca565ca | |||
| 236a7b4fd2 | |||
| e1f387dbf7 | |||
| 3d113dd31e | |||
| e0e57f0336 | |||
| e775b58d41 | |||
| 5b070a8478 | |||
| 8a61193500 | |||
| fad522b8fe | |||
| 0acac205af | |||
| 069d4c61b7 | |||
| 7c113f5700 | |||
| dfdaf4225b | |||
| bd39ab2467 | |||
| 6ce1eb3f2d | |||
| ce7d4fe9a2 | |||
| a65bb2f419 | |||
| cbc868448b | |||
| 9c336a0b56 | |||
| 0f0d09399e | |||
| e13a1162a9 | |||
| 57ebed903f | |||
| 4478193d8a | |||
| ee948395e3 | |||
| 0616b051ae | |||
| 4d155d0505 | |||
| a169e733d9 | |||
| f081e237c8 | |||
| f412d387fa | |||
| 67354b43e2 | |||
| 39135a4000 | |||
| eaa014cebd | |||
| 856e657f81 | |||
| 3a609e5b66 | |||
| d0022dd599 | |||
| 7ba6eed453 | |||
| beeb6ecc0a | |||
| 7cdc17a714 | |||
| 4765a950a9 | |||
| 65e8137e08 | |||
| ae546dd9ad | |||
| 8110c42be0 | |||
| bd2b5ae5fc | |||
| 2ddfffa6a9 | |||
| a4b54e861d | |||
| c6785f1a4f | |||
| 83fe621c56 | |||
| b3a93e17eb | |||
| 546a391af3 | |||
| 786cf9bd8b | |||
| dfd6fe062d | |||
| b6ef818613 | |||
| b0f9e9c30a | |||
| 7e5c88ddc3 | |||
| 9824c3e00f | |||
| 9171d5ed11 | |||
| a1830ebb82 | |||
| 22d2a3d9bf | |||
| c9b97fc965 | |||
| 51f85becd5 | |||
| a8a281cfb4 | |||
| ddd1304949 | |||
| 86d70b6c02 | |||
| a22bdb39e7 | |||
| 7ae4d5ef95 | |||
| a2038cbefa | |||
| 992091eade | |||
| e3bfead0c5 | |||
| 0de96141fd | |||
| fa18d15c3c | |||
| ea9dbf2371 | |||
| d34e3ec7a9 | |||
| c8833a36af | |||
| a067cb0c0f | |||
| 999c8327bd | |||
| c2ec73c70a | |||
| 702f782fc1 | |||
| 25dbcaaf83 | |||
| b00d454a24 | |||
| dbc921d56d | |||
| 438fefa7a3 | |||
| 5d74eac814 | |||
| 9fb62e1e25 | |||
| 3e366ea73b | |||
| 0ff895bffa | |||
| c5bb120280 | |||
| 4b26a92b37 | |||
| 0a8453b4d2 | |||
| c9872a61b6 | |||
| 149a1aa278 | |||
| 13d0e1b682 | |||
| 6f9be2a9f8 | |||
| 93ba98d993 | |||
| 940ee3df06 | |||
| 2e7bab10fd | |||
|  | 3ed70a37ea | ||
| fe8f80b9d9 | |||
| d81fb32fb9 | |||
| 2877b5532c | |||
| b938b21395 | |||
| 58836359cc | |||
| 5edb0e1331 | |||
| 0f0d0b5d58 | |||
| 46c1887cbe | |||
| 5f231c2212 | |||
| 4e97ce86aa | |||
| 315a7cb29e | |||
| aa7cc503f2 | |||
| 4bbe7e5a80 | |||
| d9c05f38d2 | |||
| cd0c4c9650 | |||
| fc3407f104 | |||
| 3a5544206b | |||
| e17e2f7fb8 | |||
|  | d32c95f143 | ||
| 6d8a8ab018 | |||
| a7dce8fa78 | |||
| ca73ff8e19 | |||
| d01ad10d7d | |||
| 81041ee43c | |||
|  | 6e004c2ae4 | ||
| 0e2fac5b22 | |||
| 269da7f155 | |||
| 3cb6b73ee0 | |||
| a938ee1efb | |||
| 6ea5e2e5a6 | |||
| 617dfb54e0 | |||
| d23e005985 | |||
| e5207f5bc5 | |||
| c96cea8db0 | |||
| 0a8e71d76a | |||
| cf1fd32b08 | |||
| cc4224ea1f | |||
| f4c148bc58 | |||
| 022297ad3f | |||
| 5180d6fc3e | |||
| eeebbff70d | |||
| afc6aeea15 | |||
| 486515eddd | |||
| 0e21699cd1 | |||
| f1678ef7cf | |||
| cea65fc76e | |||
| c9e320b72a | |||
| 555956087d | |||
| b3f468f901 | |||
| f5f7511781 | |||
| 4be1d93f60 | |||
| 7d684608ef | |||
| 2c7fd320eb | |||
| 88ee82e1c6 | |||
| d6402c624e | |||
| 8b9c93bc10 | |||
| 4f5e261d01 | |||
| cf455aebe6 | |||
| 1380d5f8e1 | |||
| 5f65260bfe | |||
| 11f0dcfc01 | |||
| 555b7886a4 | |||
| 3707a6c0ce | |||
| 4c8d92b4c3 | |||
| 8bbd33c896 | |||
| ac33a3580f | |||
| a64a32fbe6 | |||
| 9493e97a38 | |||
| 88bd770260 | |||
| a7bd33b7bf | |||
| 73c724a2e5 | |||
| d8cf3c6726 | |||
| 15dee238b5 | |||
| c70626734e | |||
| 5a765ea1bc | |||
| 8215f9d2c6 | |||
| d2e6d2ec80 | |||
| 3718181e8f | |||
| 0d825cf424 | |||
| 28a804d988 | |||
| 9e4bb9d678 | |||
| 9c40d7da3d | |||
| 2b76ad0aa9 | |||
| e4b619e050 | |||
| 36c09feaf2 | |||
| 2d68321503 | |||
| 85455ab21c | |||
| 18d63eb980 | |||
| 2e429e9704 | |||
| f4af28059b | |||
| c1476bd075 | |||
| 16c720fddd | |||
| 8b4b4a5eca | |||
| 32e6e5b7e2 | |||
| a9f7fd8e32 | |||
| 95be1a26f2 | |||
| ef9b31aee0 | |||
| df3c01ff0a | |||
| 4704c5a33d | |||
| 225c06550a | |||
| f0987614c6 | |||
| 269c2876f3 | |||
| 168d6acf7c | |||
| a5f718e257 | |||
| 4f68459582 | |||
| 442db122cf | |||
| 580d757be2 | |||
| 47b0f6d2d8 | |||
| 3f6f6ebc2b | |||
| 2645ea29d6 | |||
| 79f2041565 | |||
| 4a7567f288 | |||
| 8a890ed6ed | |||
| 3d90df6897 | |||
| 681c13144a | |||
| b64f2e6d32 | |||
| 428eabb1bd | |||
| 2162e83bce | |||
| 6142022283 | |||
| e6d9c8250f | |||
| 46178e723b | |||
| 605f55acd2 | |||
| 0f8b69aa60 | |||
| 551d8ec480 | |||
| fc48446ec4 | |||
| 3644b83ac6 | |||
| cd73791b6f | |||
| 03de71df2e | |||
| 83d5d3faf4 | |||
| 0c8bec4c89 | |||
| 7fc93817c1 | |||
| d0a00031a1 | |||
| 6ebc5aa0c2 | |||
| 8a6b4bb49e | |||
| 20799b9a3e | |||
| ec3afc615c | |||
| da692ccfc3 | |||
| 53b89f3a18 | |||
| 58cded28d3 | |||
| 592c5f3732 | |||
| f44a78a5f5 | |||
| e0bdd5dfdc | |||
| 99c0f06b72 | |||
| 66fc6df3d7 | |||
| a36425a905 | |||
| d920fee6d4 | |||
| 23590be5de | |||
| 94acc3c93b | |||
| 5616326a3b | |||
| 7601860c5c | |||
| 8b43d785cc | |||
| b62d3a0b7d | |||
| fad73c7213 | |||
| 2403c7c2b0 | |||
| fa090bf920 | |||
| a83ee86340 | |||
| 204955bcce | |||
| ee56e9543a | |||
| 96fdff6ffd | |||
| 58b007cbb3 | |||
| 4f0c139889 | |||
| c584c24fce | |||
| 85e5cee154 | |||
| 5af91981f1 | |||
| 2fe4f08059 | |||
| 83796f345a | |||
| c1e21364a6 | |||
| 067d9d0d3b | |||
| 03f527d83e | |||
| ced05a4586 | |||
| 43fe06206a | |||
| 023657558e | |||
| 9b0b726c80 | |||
| 4ee67321c4 | |||
| 59f1f2e59b | |||
| 0766d48b7c | |||
| e18903b9e9 | |||
| d0eecdead2 | |||
| cc4a83a033 | |||
| 1cf911bbde | |||
| 36d73d5023 | |||
| c395242e3e | |||
| cd9cd7cc5d | |||
| acbb8a0c07 | |||
| b9d8528599 | |||
| 4971326eca | |||
| 09d1047260 | |||
| 02dbd493c2 | |||
| b17931e7bd | |||
| 2a4570eafc | |||
| c9514d3a6d | |||
| 072805efc7 | |||
| 369ff26627 | |||
| c5abbbbd2d | |||
| d974639f1e | |||
| 26efde316b | |||
| fafe50f80a | |||
| 41504469db | |||
| 03b3ddd98b | |||
| 89d919f2be | |||
| b53cfd5504 | |||
| 31022733ac | |||
| f9a8c39879 | |||
| a812c2dd2f | |||
| 217e977f0d | |||
| 04c301d1ac | |||
| 7f0c425389 | |||
| 1ede1c423b | |||
| 7cb064896a | |||
| 0c5e2862ca | |||
| 30d4507f54 | |||
| b6c6d89455 | |||
| 9d218ee534 | |||
| 11116d8cab | |||
| 58d754bbde | |||
| 8f25c123dd | |||
| 76e214fc08 | |||
| 2b5380f8d6 | |||
| 844a129094 | |||
| a3090cca9d | |||
| b7b5159e9c | |||
| 0f8bc2c950 | |||
| 69f5c49f45 | |||
| 9b308e6fb8 | |||
| 3e3f91128b | |||
| 0d1aae0ef7 | |||
| 4d022f0480 | |||
| 153e20d00e | |||
| a9a8171dd6 | |||
| bf5c3b59b2 | |||
| 607c432bdb | |||
| ae5c010770 | |||
| d5e432437f | |||
| 9f99ebce01 | |||
| 64ee899b84 | |||
| e0e0c1658b | |||
| 2c586f667c | |||
| 64164ef6c1 | |||
| 22343c0731 | |||
| f4ec1a4c60 | |||
| c1c33cceb1 | |||
| a3e975b2ba | |||
| 06e705a687 | |||
| d56eb6c867 | |||
| 9cbca864e3 | |||
| abb4378694 | |||
| 0eb698d9a4 | |||
| 15ea9f2093 | |||
| d47aca0923 | |||
| 1ac50e9959 | |||
| 6adfbe3a96 | |||
| 59f36e62e9 | |||
| 54af116009 | |||
| 38fbec8e3b | |||
| babbfc55e4 | |||
| 2511e18d69 | |||
| 29658c70a0 | |||
| 96311ee43d | |||
| bd33b09052 | |||
| 8055003b47 | |||
| 1257492f85 | |||
| 1107b7f4ef | |||
| a1a1171240 | |||
| 46c02e5df1 | |||
| 2e9efc57de | |||
| acecadef17 | |||
| 19394b5e69 | |||
| de999e197f | |||
| 9d95687d3c | |||
| aa9dfb4ab8 | |||
| 9c5b44efb3 | |||
| ac587a67e6 | |||
| 59428140a8 | |||
| 60bdb59d71 | |||
| be52871de8 | |||
| b7934cf357 | |||
| dbfbeef90a | |||
| 00943c9cdf | |||
| 8745c6a16a | |||
| 433ba4b58f | |||
| d40376e524 | |||
| a2982f88f5 | |||
| 1642f7abd9 | |||
| a10d2184ff | |||
| 522435f096 | |||
| 79b30290c0 | |||
| f8b8626859 | |||
| b061b85a08 | |||
| 3870db1c88 | |||
| 1be1070eb4 | |||
| 2696e663cf | |||
| 1e1f7df86d | |||
| 1d8ded8fd3 | |||
| 197825123a | |||
| 422b2e6db1 | |||
| 1973e0b5bf | |||
| 8258cf93a9 | |||
| 1d49bd5947 | |||
| 44317d1519 | |||
| 48e08fcc69 | |||
| 1a3ce6e623 | |||
| fb122f3e70 | |||
| 7cca6039cc | |||
| 118e3dba39 | |||
| 87070710fa | |||
| 38499c3d4a | |||
| 5d754d968b | |||
| d543d436bc | |||
| 12b54f99af | |||
| 2a95d7e643 | |||
| e3d3cacfa4 | |||
| 4b13491a0e | |||
| 85d516d1e9 | |||
| ac58b6a7e3 | |||
| 2cc6126765 | |||
| f94b085850 | |||
| c9822a491b | |||
| 23b2d60295 | |||
| f4bc9eed39 | |||
| e310f188b0 | |||
| 6c1571188c | |||
| 945d2fa284 | |||
| 020095f1ff | |||
| b165a76e62 | |||
| 03c8830672 | |||
| 38448da89b | |||
| 2ade5aff91 | |||
| 29bf6e80ec | |||
| 027e927e1b | |||
| fa061f88e2 | |||
| 1d01b65b5f | |||
| 2c2b364167 | |||
| 7c5fc9bf7c | |||
| 193d22ff20 | |||
| 0f3b553ba2 | |||
| 4eb1013446 | |||
| be1b2563ec | |||
| 1c9c2f1e70 | |||
| 52dfded741 | |||
| 0a115e5cf4 | |||
| c349af999b | |||
| 9a5709a34d | |||
| 28d311160b | |||
| 1ca7081a40 | |||
| 5ce8ebe82c | |||
| a7a88b29b9 | |||
| cd7b982385 | |||
| ee59100075 | |||
| f808ac58ef | |||
| f33ada5396 | |||
| 984d781f2f | |||
| dd33e1e8bc | |||
| 2950de29e5 | |||
| 50a8799f9d | |||
| bdb0ce6fc7 | |||
| 18ec2bca96 | |||
| 937ef48794 | |||
| 6331f13e9a | |||
| 5213a2ff8e | |||
| 087d7452fd | |||
| 703094c924 | |||
| eea645c865 | |||
| 324832a189 | |||
| d55d735c51 | |||
| e3ff1b9609 | |||
| 70c31966ca | |||
| 0e4188882f | |||
| 6bf0ce92ba | |||
| d51bdc5086 | |||
| f04f262cee | |||
| 0f172055ef | |||
| e5dd4363f1 | |||
| a3a48bbaac | |||
| 5e716fb9a8 | |||
| 11a36153cc | |||
| 8bee354f04 | |||
| f7dd2b5ce7 | |||
| 8ca10c00bb | |||
| 905c7e8eda | |||
| d4c5e849bf | |||
| 8250a2a021 | |||
| 01b3df7b8c | |||
| daa6e4aff5 | |||
| 23bcb26a58 | |||
| e55f60c30b | |||
| 0d0c16e16d | |||
| 540d5cce7c | |||
| a548b00979 | |||
| 4b7ca6d565 | |||
| 0473fa238c | |||
| cfc7119697 | |||
| 22a6520d3e | |||
| fb25e91191 | |||
| c116b270b6 | |||
| aa2d598689 | |||
| 5ef3bb746b | |||
| 037616e271 | |||
| abbea906f1 | |||
| 9132e216c9 | |||
| 12a7e3c4af | |||
| b40c093917 | |||
| 7ac12455c8 | |||
| 5043eec7a2 | |||
| cf31f53e01 | |||
| cd22d76fa7 | |||
| c8759843f7 | |||
| 781bbcc012 | |||
| 6da29c0686 | |||
| fcdb6fc45a | |||
| e785a99bd7 | |||
| 121e513fdd | |||
| 9cf01ab54f | |||
| c655107681 | |||
| 91a5af6a9a | |||
| 86e74c0a6f | |||
| d8f01f21a0 | |||
| 9fb8626d8c | |||
| 1c52e04cdb | |||
| 798128256e | |||
| 72c2df47fd | |||
| c9c6d4c0c1 | |||
| 2f4f9f3003 | |||
| 22e8f8e5d6 | |||
| 04cf8c3d9a | |||
| 52198be543 | |||
| 80fd5a489b | |||
| b187043ee1 | |||
| 8965752055 | |||
| b796620267 | |||
| 62df81bb4e | |||
| 0a8e0f6178 | |||
| f705020aaa | |||
| 78bd3b9853 | |||
| 992b283597 | |||
| 8fc1ff1d59 | |||
| 3bbde61f39 | |||
| 8d955c4b9d | |||
| 18593c530b | |||
| 78903cd4eb | |||
| eaa143f7d7 | |||
| bcb0e42fa2 | |||
| 8eed435302 | |||
| 0e4a63057f | |||
| 1af5faa440 | |||
| 7412217b0c | |||
| 3e749d75b7 | |||
| 846b5c87c9 | |||
| 7b5e84f80b | |||
| 27822a8a66 | |||
| 36f4e7ec37 | |||
| 187e84ad65 | |||
| 195fe221c4 | |||
| 6f9c19bbf6 | |||
| 65bdab4f7e | |||
| c4d5fcfc22 | |||
| 43232afa62 | |||
| 18908a01d7 | |||
| b22fbfb3bc | |||
| 57aaea88b6 | |||
| 140949c5ea | |||
| 639241e0d3 | |||
| e0eb42bc2d | |||
| 7990b21cc5 | |||
| 2ba5c97709 | |||
| 33b7c85fc2 | |||
| 7f6c02ffdf | |||
| f368616e6f | |||
| dd632f4203 | |||
| a8f3ae501b | |||
| ea76963ac2 | |||
| 99dfa97958 | |||
| f68270a5b3 | |||
| 542ed81034 | |||
| 404a11f5e7 | |||
| 411221070e | |||
| 25d35d0c76 | |||
| c72904d61c | |||
| b94c9acd26 | |||
| bdb4988569 | |||
| 1dafd1352a | |||
| 31fbd3cad7 | |||
| 848bc5ec10 | |||
| 4d0ad826a0 | |||
| 6955820fcc | |||
| ef530624b9 | |||
| 9b9e7dd88f | |||
| a13cc9e961 | |||
| 0d2b923378 | |||
| fba84c8ac8 | |||
| db10fe1b2c | |||
| 175dd980f8 | |||
| 8364020671 | |||
| eba44cd394 | |||
| b3bac8015a | |||
| 0b48afd251 | |||
| 19857930a4 | |||
| d0dbe3ed2f | |||
| 8b7e78b63a | |||
| 92a4ecb523 | |||
| 6a5ad4d728 | |||
| be4aa8daac | |||
| b5eac37782 | |||
| b1ad3c5a39 | |||
| ba16bad029 | |||
| ca8ae4cd72 | |||
| 53d35d74b3 | |||
| 49c139e235 | |||
| caf9c821f3 | |||
| ca4c6db96f | |||
| 6b2298c752 | |||
| a1bf43def9 | |||
| 15e9254e00 | |||
| afe5a72c6f | |||
| 750a8b9ecf | |||
| 27fc3f93e0 | |||
| 8166d4b99b | |||
| b61d2ae2eb | |||
| 4790fe0aea | |||
| bc37b11cee | |||
| 223fed910f | |||
| b85ab7b061 | |||
| 888dc299c9 | |||
| e113dc28ed | |||
| 31e55d2307 | |||
| e90645f248 | |||
| 4bb7ba2571 | |||
| 8d31c25bf8 | |||
| c7ee1c28b2 | |||
| 99b09c8b28 | |||
| a328c4425a | |||
| c0f61ca896 | |||
| 86e70c0961 | |||
| d87a3a039f | |||
| 6279a2c40a | |||
| f377ebea88 | |||
| 7373fef964 | |||
| 41ef86dbda | |||
| 7bc2b2336d | |||
| 0a615e6d78 | |||
| 8f928e16e1 | |||
| 72bc8da1e7 | |||
| 51e349a5db | |||
| 3cbb19ba2c | |||
| ae3a5bf45d | |||
| 9c667f4b78 | |||
| 21195e1bcb | |||
| 03117ac565 | |||
| d13fbdf176 | |||
| 7cecc0e0b6 | |||
| 203e781f5d | |||
| 3eb6cd77cd | |||
| 51855b2405 | |||
| 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 | |||
| 3347a6d189 | |||
| 47b3b42949 | |||
| e985631621 | |||
| e15034bfa4 | |||
| e5dbcd25dd | |||
| 3714c02c12 | |||
| fa636b4146 | |||
| 3d825aebc3 | |||
| 629884a396 | |||
| e7a0fa4e8f | |||
| 1a41f37a9d | |||
| 07a65e0bb5 | |||
| 615f7f99c3 | |||
| 3de5558ed4 | |||
| 6bbe3a271f | |||
| 8bee29f683 | |||
| c40f0fdcb9 | |||
| 30d1453a12 | |||
| 75fa88b00d | |||
| 0f817ad212 | |||
| d7b46ae0d4 | |||
| b6be14ecca | |||
| 33dbfc6f69 | |||
| fcacdcd544 | |||
| dd2fc5a86f | |||
| 0f8a6f6bde | |||
| 1efd94181d | |||
| 71ff0232aa | |||
| 63921cd984 | |||
| 051e03bed3 | |||
| a051394f4f | |||
| b872babe45 | |||
| 5a9cabc4bd | |||
| 3ba630684a | |||
| 498cd12f94 | |||
| 062848f2e4 | |||
| d4b4547718 | |||
| 22cd440dd7 | |||
| 6fc64526d4 | |||
| 08075dfafe | |||
| efcb25622e | |||
| 5ebf29d1fb | |||
| b7d0ce3c97 | |||
| e20929aec4 | |||
| f74167cc65 | |||
| c55cd5342e | |||
| 237d89d611 | |||
| aff9b52f1c | |||
| d8cc6ed06e | |||
| dffafa54c3 | |||
| 776a3af497 | |||
| 5b3d9e8d64 | |||
| 90b7d74a0c | |||
| 5b596c76e0 | |||
| c59e601e2e | |||
| 5ce71ee6f6 | |||
| 97dadf517a | |||
| a739e874bf | |||
| c73cb14615 | |||
| d9464f7b90 | |||
| aeee41680e | |||
| c914f8c44a | |||
| abfda3627c | |||
| 5b5bfa02db | 
							
								
								
									
										25
									
								
								.github/workflows/build.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								.github/workflows/build.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | |||||||
|  |  | ||||||
|  | name: Build | ||||||
|  | on: [push] | ||||||
|  | jobs: | ||||||
|  |   build: | ||||||
|  |     runs-on: ubuntu-latest | ||||||
|  |     steps: | ||||||
|  |       - uses: actions/checkout@v2 | ||||||
|  |       - uses: actions/setup-java@v1 | ||||||
|  |         with: | ||||||
|  |           java-version: 17 | ||||||
|  |       - 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 to InmoNexus | ||||||
|  |         continue-on-error: true | ||||||
|  |         run: ./gradlew publishAllPublicationsToInmoNexusRepository | ||||||
|  |         env: | ||||||
|  |           INMONEXUS_USER: ${{ secrets.INMONEXUS_USER }} | ||||||
|  |           INMONEXUS_PASSWORD: ${{ secrets.INMONEXUS_PASSWORD }} | ||||||
							
								
								
									
										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: 17 | ||||||
|  |       - 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 | ||||||
							
								
								
									
										11
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										11
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -1,4 +1,6 @@ | |||||||
| .idea | .idea | ||||||
|  | .vscode | ||||||
|  | .kotlin | ||||||
| out/* | out/* | ||||||
| *.iml | *.iml | ||||||
| target | target | ||||||
| @@ -8,7 +10,16 @@ settings.xml | |||||||
| .gradle/ | .gradle/ | ||||||
| build/ | build/ | ||||||
| out/ | out/ | ||||||
|  | bin/ | ||||||
|  |  | ||||||
| secret.gradle | secret.gradle | ||||||
|  | local.properties | ||||||
|  | kotlin-js-store | ||||||
|  |  | ||||||
| publishing.sh | publishing.sh | ||||||
|  |  | ||||||
|  | local.* | ||||||
|  | local/ | ||||||
|  | **/*.local.* | ||||||
|  |  | ||||||
|  | .kotlin/ | ||||||
|   | |||||||
							
								
								
									
										13
									
								
								.travis.yml
									
									
									
									
									
								
							
							
						
						
									
										13
									
								
								.travis.yml
									
									
									
									
									
								
							| @@ -1,13 +0,0 @@ | |||||||
| language: java |  | ||||||
| install: true |  | ||||||
|  |  | ||||||
| os: linux |  | ||||||
| dist: trusty |  | ||||||
| jdk: oraclejdk8 |  | ||||||
|  |  | ||||||
| jobs: |  | ||||||
|   include: |  | ||||||
|     - stage: build |  | ||||||
|       script: ./gradlew build -s -x jvmTest -x jsIrTest -x jsIrBrowserTest -x jsIrNodeTest -x jsLegacyTest -x jsLegacyBrowserTest -x jsLegacyNodeTest |  | ||||||
|     - state: test |  | ||||||
|       script: ./gradlew allTests |  | ||||||
							
								
								
									
										2570
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										2570
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										84
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										84
									
								
								README.md
									
									
									
									
									
								
							| @@ -1 +1,85 @@ | |||||||
| # 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 | ||||||
|  |  | ||||||
|  | ## Gradle Templates | ||||||
|  |  | ||||||
|  | All templates can be used by applying them in your project's build.gradle files using the `apply from` directive. For example: | ||||||
|  |  | ||||||
|  | ```gradle | ||||||
|  | apply from: "$defaultProject" | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | In the sample has been used `defaultProject.gradle` as a basic template. | ||||||
|  |  | ||||||
|  | The project includes a collection of Gradle templates to simplify project setup and configuration. These templates are located in the `gradle/templates` directory and can be used to quickly set up different types of projects: | ||||||
|  |  | ||||||
|  | ### Project Setup Templates | ||||||
|  |  | ||||||
|  | * `defaultProject.gradle` (usage `apply from: "$defaultProject"`) - Basic project configuration | ||||||
|  | * `defaultProjectWithSerialization.gradle` (usage `apply from: "$defaultProjectWithSerialization"`) - Project configuration with Kotlin Serialization support | ||||||
|  | * `mppJavaProject.gradle` (usage `apply from: "$mppJavaProject"`) - Multiplatform project with Java support | ||||||
|  | * `mppAndroidProject.gradle` (usage `apply from: "$mppAndroidProject"`) - Multiplatform project with Android support | ||||||
|  |  | ||||||
|  | ### Multiplatform Configuration Templates | ||||||
|  |  | ||||||
|  | * `enableMPPAndroid.gradle` (usage `apply from: "$enableMPPAndroid"`) - Enable Android target in multiplatform project | ||||||
|  | * `enableMPPJs.gradle` (usage `apply from: "$enableMPPJs"`) - Enable JavaScript target in multiplatform project | ||||||
|  | * `enableMPPJvm.gradle` (usage `apply from: "$enableMPPJvm"`) - Enable JVM target in multiplatform project | ||||||
|  | * `enableMPPNativeArm64.gradle` (usage `apply from: "$enableMPPNativeArm64"`) - Enable ARM64 native target | ||||||
|  | * `enableMPPNativeX64.gradle` (usage `apply from: "$enableMPPNativeX64"`) - Enable x64 native target | ||||||
|  | * `enableMPPWasmJs.gradle` (usage `apply from: "$enableMPPWasmJs"`) - Enable WebAssembly JavaScript target | ||||||
|  |  | ||||||
|  | ### Compose Integration Templates | ||||||
|  |  | ||||||
|  | * `addCompose.gradle` (usage `apply from: "$addCompose"`) - Basic Compose configuration | ||||||
|  | * `addComposeForAndroid.gradle` (usage `apply from: "$addComposeForAndroid"`) - Compose configuration for Android | ||||||
|  | * `addComposeForDesktop.gradle` (usage `apply from: "$addComposeForDesktop"`) - Compose configuration for Desktop | ||||||
|  | * `addComposeForJs.gradle` (usage `apply from: "$addComposeForJs"`) - Compose configuration for JavaScript | ||||||
|  |  | ||||||
|  | ### Publishing Templates | ||||||
|  |  | ||||||
|  | * `publish.gradle` (usage `apply from: "$publish"`) - General publishing configuration | ||||||
|  | * `publish_jvm.gradle` (usage `apply from: "$publish_jvm"`) - JVM-specific publishing configuration | ||||||
|  | * `publish.kpsb` and `publish_jvm.kpsb` (usage `apply from: "$publish_kpsb"` and `apply from: "$publish_jvm_kpsb"`) - Publishing configuration for Kotlin Multiplatform and JVM | ||||||
|  |  | ||||||
|  | ### Combined Project Templates | ||||||
|  |  | ||||||
|  | * `mppJvmJsWasmJsLinuxMingwProject.gradle` (usage `apply from: "$mppJvmJsWasmJsLinuxMingwProject"`) - Multiplatform project for JVM, JS, Wasm, Linux, and MinGW | ||||||
|  | * `mppJvmJsWasmJsAndroidLinuxMingwLinuxArm64Project.gradle` (usage `apply from: "$mppJvmJsWasmJsAndroidLinuxMingwLinuxArm64Project"`) - Multiplatform project with additional Android and ARM64 support | ||||||
|  | * `mppComposeJvmJsWasmJsAndroidLinuxMingwLinuxArm64Project.gradle` (usage `apply from: "$mppComposeJvmJsWasmJsAndroidLinuxMingwLinuxArm64Project"`) - Multiplatform project with Compose support | ||||||
|  | * `mppProjectWithSerializationAndCompose.gradle` (usage `apply from: "$mppProjectWithSerializationAndCompose"`) - Multiplatform project with both Serialization and Compose support | ||||||
|   | |||||||
							
								
								
									
										1
									
								
								_config.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								_config.yml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | theme: jekyll-theme-cayman | ||||||
							
								
								
									
										17
									
								
								android/alerts/common/build.gradle
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								android/alerts/common/build.gradle
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | |||||||
|  | plugins { | ||||||
|  |     id "org.jetbrains.kotlin.multiplatform" | ||||||
|  |     id "org.jetbrains.kotlin.plugin.serialization" | ||||||
|  |     id "com.android.library" | ||||||
|  | } | ||||||
|  |  | ||||||
|  | apply from: "$mppAndroidProject" | ||||||
|  |  | ||||||
|  | kotlin { | ||||||
|  |     sourceSets { | ||||||
|  |         androidMain { | ||||||
|  |             dependencies { | ||||||
|  |                 api libs.android.appCompat.resources | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1 @@ | |||||||
|  | <manifest/> | ||||||
							
								
								
									
										55
									
								
								android/alerts/common/src/androidMain/kotlin/AlertDialog.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								android/alerts/common/src/androidMain/kotlin/AlertDialog.kt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,55 @@ | |||||||
|  | @file:Suppress("NOTHING_TO_INLINE", "unused") | ||||||
|  |  | ||||||
|  | package dev.inmo.micro_utils.android.alerts.common | ||||||
|  |  | ||||||
|  | import android.app.AlertDialog | ||||||
|  | import android.content.Context | ||||||
|  | import android.content.DialogInterface | ||||||
|  |  | ||||||
|  | typealias AlertDialogCallback = (DialogInterface) -> Unit | ||||||
|  |  | ||||||
|  | inline fun Context.createAlertDialogTemplate( | ||||||
|  |     title: String? = null, | ||||||
|  |     positivePair: Pair<String, AlertDialogCallback?>? = null, | ||||||
|  |     neutralPair: Pair<String, AlertDialogCallback?>? = null, | ||||||
|  |     negativePair: Pair<String, AlertDialogCallback?>? = null | ||||||
|  | ): AlertDialog.Builder { | ||||||
|  |     val builder = AlertDialog.Builder(this) | ||||||
|  |  | ||||||
|  |     title ?.let { | ||||||
|  |         builder.setTitle(title) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     positivePair ?. let { | ||||||
|  |         builder.setPositiveButton(it.first) { di, _ -> it.second ?. invoke(di) } | ||||||
|  |     } | ||||||
|  |     negativePair ?. let { | ||||||
|  |         builder.setNegativeButton(it.first) { di, _ -> it.second ?. invoke(di) } | ||||||
|  |     } | ||||||
|  |     neutralPair ?. let { | ||||||
|  |         builder.setNeutralButton(it.first) { di, _ -> it.second ?. invoke(di) } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return builder | ||||||
|  | } | ||||||
|  |  | ||||||
|  | inline fun Context.createAlertDialogTemplateWithResources( | ||||||
|  |     title: Int? = null, | ||||||
|  |     positivePair: Pair<Int, AlertDialogCallback?>? = null, | ||||||
|  |     neutralPair: Pair<Int, AlertDialogCallback?>? = null, | ||||||
|  |     negativePair: Pair<Int, AlertDialogCallback?>? = null | ||||||
|  | ): AlertDialog.Builder = createAlertDialogTemplate( | ||||||
|  |     title ?.let { getString(it) }, | ||||||
|  |     positivePair ?.let { getString(it.first) to it.second }, | ||||||
|  |     neutralPair ?.let { getString(it.first) to it.second }, | ||||||
|  |     negativePair ?.let { getString(it.first) to it.second } | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | inline fun AlertDialog.setDismissChecker(noinline checker: () -> Boolean) : AlertDialog { | ||||||
|  |     setOnDismissListener { | ||||||
|  |         if (!checker()) { | ||||||
|  |             show() | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     return this | ||||||
|  | } | ||||||
| @@ -0,0 +1,38 @@ | |||||||
|  | @file:Suppress("NOTHING_TO_INLINE", "unused") | ||||||
|  |  | ||||||
|  | package dev.inmo.micro_utils.android.alerts.common | ||||||
|  |  | ||||||
|  | import android.app.AlertDialog | ||||||
|  | import android.content.Context | ||||||
|  | import android.view.View | ||||||
|  |  | ||||||
|  | inline fun <T: View> Context.createCustomViewAlertDialog( | ||||||
|  |     title: String? = null, | ||||||
|  |     positivePair: Pair<String, AlertDialogCallback?>? = null, | ||||||
|  |     neutralPair: Pair<String, AlertDialogCallback?>? = null, | ||||||
|  |     negativePair: Pair<String, AlertDialogCallback?>? = null, | ||||||
|  |     show: Boolean = true, | ||||||
|  |     viewCreator: (Context) -> T | ||||||
|  | ): AlertDialog = createAlertDialogTemplate( | ||||||
|  |     title, positivePair, neutralPair, negativePair | ||||||
|  | ).apply { | ||||||
|  |     setView(viewCreator(this@createCustomViewAlertDialog)) | ||||||
|  | }.create().apply { | ||||||
|  |     if (show) show() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | inline fun <T: View> Context.createCustomViewAlertDialogWithResources( | ||||||
|  |     title: Int? = null, | ||||||
|  |     positivePair: Pair<Int, AlertDialogCallback?>? = null, | ||||||
|  |     neutralPair: Pair<Int, AlertDialogCallback?>? = null, | ||||||
|  |     negativePair: Pair<Int, AlertDialogCallback?>? = null, | ||||||
|  |     show: Boolean = true, | ||||||
|  |     viewCreator: (Context) -> T | ||||||
|  | ): AlertDialog = createCustomViewAlertDialog( | ||||||
|  |     title ?.let { getString(it) }, | ||||||
|  |     positivePair ?.let { getString(it.first) to it.second }, | ||||||
|  |     neutralPair ?.let { getString(it.first) to it.second }, | ||||||
|  |     negativePair ?.let { getString(it.first) to it.second }, | ||||||
|  |     show, | ||||||
|  |     viewCreator | ||||||
|  | ) | ||||||
| @@ -0,0 +1,45 @@ | |||||||
|  | @file:Suppress("NOTHING_TO_INLINE", "unused") | ||||||
|  |  | ||||||
|  | package dev.inmo.micro_utils.android.alerts.common | ||||||
|  |  | ||||||
|  | import android.app.AlertDialog | ||||||
|  | import android.content.Context | ||||||
|  | import androidx.annotation.StringRes | ||||||
|  |  | ||||||
|  | inline fun Context.createSimpleTextAlertDialog( | ||||||
|  |     text: String, | ||||||
|  |     title: String? = null, | ||||||
|  |     positivePair: Pair<String, AlertDialogCallback?>? = null, | ||||||
|  |     neutralPair: Pair<String, AlertDialogCallback?>? = null, | ||||||
|  |     negativePair: Pair<String, AlertDialogCallback?>? = null, | ||||||
|  |     show: Boolean = true | ||||||
|  | ): AlertDialog = createAlertDialogTemplate( | ||||||
|  |     title, | ||||||
|  |     positivePair, | ||||||
|  |     neutralPair, | ||||||
|  |     negativePair | ||||||
|  | ).apply { | ||||||
|  |     setMessage(text) | ||||||
|  | }.create().apply { | ||||||
|  |     if (show) { | ||||||
|  |         show() | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | inline fun Context.createSimpleTextAlertDialog( | ||||||
|  |     @StringRes | ||||||
|  |     text: Int, | ||||||
|  |     @StringRes | ||||||
|  |     title: Int? = null, | ||||||
|  |     positivePair: Pair<Int, AlertDialogCallback?>? = null, | ||||||
|  |     neutralPair: Pair<Int, AlertDialogCallback?>? = null, | ||||||
|  |     negativePair: Pair<Int, AlertDialogCallback?>? = null, | ||||||
|  |     show: Boolean = true | ||||||
|  | ): AlertDialog = createSimpleTextAlertDialog( | ||||||
|  |     getString(text), | ||||||
|  |     title ?.let { getString(it) }, | ||||||
|  |     positivePair ?.let { getString(it.first) to it.second }, | ||||||
|  |     neutralPair ?.let { getString(it.first) to it.second }, | ||||||
|  |     negativePair ?.let { getString(it.first) to it.second }, | ||||||
|  |     show | ||||||
|  | ) | ||||||
							
								
								
									
										18
									
								
								android/alerts/recyclerview/build.gradle
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								android/alerts/recyclerview/build.gradle
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | |||||||
|  | plugins { | ||||||
|  |     id "org.jetbrains.kotlin.multiplatform" | ||||||
|  |     id "org.jetbrains.kotlin.plugin.serialization" | ||||||
|  |     id "com.android.library" | ||||||
|  | } | ||||||
|  |  | ||||||
|  | apply from: "$mppAndroidProject" | ||||||
|  |  | ||||||
|  | kotlin { | ||||||
|  |     sourceSets { | ||||||
|  |         commonMain { | ||||||
|  |             dependencies { | ||||||
|  |                 api internalProject("micro_utils.android.alerts.common") | ||||||
|  |                 api internalProject("micro_utils.android.recyclerview") | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1 @@ | |||||||
|  | <manifest/> | ||||||
| @@ -0,0 +1,65 @@ | |||||||
|  | @file:Suppress("unused") | ||||||
|  |  | ||||||
|  | package dev.inmo.micro_utils.android.alerts.recyclerview | ||||||
|  |  | ||||||
|  | import android.app.AlertDialog | ||||||
|  | import android.content.Context | ||||||
|  | import android.content.DialogInterface | ||||||
|  | import android.view.ViewGroup | ||||||
|  | import android.widget.TextView | ||||||
|  | import dev.inmo.micro_utils.android.alerts.common.AlertDialogCallback | ||||||
|  | import dev.inmo.micro_utils.android.recyclerview.* | ||||||
|  |  | ||||||
|  | data class AlertAction( | ||||||
|  |     val title: String, | ||||||
|  |     val callback: (DialogInterface) -> Unit | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | class ActionViewHolder( | ||||||
|  |     container: ViewGroup, dialogInterfaceGetter: () -> DialogInterface | ||||||
|  | ) : AbstractStandardViewHolder<AlertAction>(container, android.R.layout.simple_list_item_1) { | ||||||
|  |     private lateinit var action: AlertAction | ||||||
|  |     private val textView: TextView | ||||||
|  |         get() = itemView.findViewById(android.R.id.text1) | ||||||
|  |  | ||||||
|  |     init { | ||||||
|  |         itemView.setOnClickListener { | ||||||
|  |             action.callback(dialogInterfaceGetter()) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun onBind(item: AlertAction) { | ||||||
|  |         action = item | ||||||
|  |         textView.text = item.title | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class ActionsRecyclerViewAdapter( | ||||||
|  |     override val data: List<AlertAction>, | ||||||
|  |     private val dialogInterfaceGetter: () -> DialogInterface | ||||||
|  | ) : RecyclerViewAdapter<AlertAction>() { | ||||||
|  |     override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AbstractViewHolder<AlertAction> = ActionViewHolder( | ||||||
|  |         parent, dialogInterfaceGetter | ||||||
|  |     ) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fun Context.createActionsAlertDialog( | ||||||
|  |     actions: List<AlertAction>, | ||||||
|  |     title: Int? = null, | ||||||
|  |     positivePair: Pair<Int, AlertDialogCallback?>? = null, | ||||||
|  |     neutralPair: Pair<Int, AlertDialogCallback?>? = null, | ||||||
|  |     negativePair: Pair<Int, AlertDialogCallback?>? = null, | ||||||
|  |     show: Boolean = true | ||||||
|  | ): AlertDialog { | ||||||
|  |     lateinit var dialogInterface: DialogInterface | ||||||
|  |  | ||||||
|  |     return createRecyclerViewDialog( | ||||||
|  |         title, positivePair, neutralPair, negativePair, show | ||||||
|  |     ) { | ||||||
|  |         ActionsRecyclerViewAdapter( | ||||||
|  |             actions | ||||||
|  |         ) { | ||||||
|  |             dialogInterface | ||||||
|  |         } | ||||||
|  |     }.also { dialogInterface = it } | ||||||
|  | } | ||||||
| @@ -0,0 +1,43 @@ | |||||||
|  | package dev.inmo.micro_utils.android.alerts.recyclerview | ||||||
|  |  | ||||||
|  | import android.app.AlertDialog | ||||||
|  | import android.content.Context | ||||||
|  | import android.widget.LinearLayout | ||||||
|  | import androidx.recyclerview.widget.LinearLayoutManager | ||||||
|  | import androidx.recyclerview.widget.RecyclerView | ||||||
|  | import dev.inmo.micro_utils.android.alerts.common.AlertDialogCallback | ||||||
|  | import dev.inmo.micro_utils.android.alerts.common.createCustomViewAlertDialogWithResources | ||||||
|  |  | ||||||
|  | fun Context.createRecyclerViewDialog( | ||||||
|  |     title: Int? = null, | ||||||
|  |     positivePair: Pair<Int, AlertDialogCallback?>? = null, | ||||||
|  |     neutralPair: Pair<Int, AlertDialogCallback?>? = null, | ||||||
|  |     negativePair: Pair<Int, AlertDialogCallback?>? = null, | ||||||
|  |     show: Boolean = true, | ||||||
|  |     layoutManager: RecyclerView.LayoutManager = LinearLayoutManager(this), | ||||||
|  |     marginOfRecyclerView: Int = 8, // dp | ||||||
|  |     recyclerViewSetUp: RecyclerView.() -> Unit = {}, | ||||||
|  |     adapterFactory: () -> RecyclerView.Adapter<*> | ||||||
|  | ): AlertDialog { | ||||||
|  |     val recyclerView = RecyclerView(this).apply { | ||||||
|  |         layoutParams = LinearLayout.LayoutParams( | ||||||
|  |             LinearLayout.LayoutParams.MATCH_PARENT, | ||||||
|  |             LinearLayout.LayoutParams.WRAP_CONTENT | ||||||
|  |         ).apply { | ||||||
|  |             setMargins(marginOfRecyclerView, marginOfRecyclerView, marginOfRecyclerView, marginOfRecyclerView) | ||||||
|  |         } | ||||||
|  |         this.layoutManager = layoutManager | ||||||
|  |         adapter = adapterFactory() | ||||||
|  |         recyclerViewSetUp() | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return createCustomViewAlertDialogWithResources( | ||||||
|  |         title, | ||||||
|  |         positivePair, | ||||||
|  |         neutralPair, | ||||||
|  |         negativePair, | ||||||
|  |         show | ||||||
|  |     ) { | ||||||
|  |         recyclerView | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										19
									
								
								android/pickers/build.gradle
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								android/pickers/build.gradle
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | |||||||
|  | plugins { | ||||||
|  |     id "org.jetbrains.kotlin.multiplatform" | ||||||
|  |     id "org.jetbrains.kotlin.plugin.serialization" | ||||||
|  |     id "com.android.library" | ||||||
|  |     alias(libs.plugins.jb.compose) | ||||||
|  |     alias(libs.plugins.kt.jb.compose) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | apply from: "$mppProjectWithSerializationAndCompose" | ||||||
|  |  | ||||||
|  | kotlin { | ||||||
|  |     sourceSets { | ||||||
|  |         androidMain { | ||||||
|  |             dependencies { | ||||||
|  |                 api project(":micro_utils.android.smalltextfield") | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										1
									
								
								android/pickers/src/androidMain/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								android/pickers/src/androidMain/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | <manifest/> | ||||||
							
								
								
									
										27
									
								
								android/pickers/src/androidMain/kotlin/Fling.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								android/pickers/src/androidMain/kotlin/Fling.kt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | |||||||
|  | package dev.inmo.micro_utils.android.pickers | ||||||
|  |  | ||||||
|  | import androidx.compose.animation.core.* | ||||||
|  |  | ||||||
|  | internal suspend fun Animatable<Float, AnimationVector1D>.fling( | ||||||
|  |     initialVelocity: Float, | ||||||
|  |     animationSpec: DecayAnimationSpec<Float>, | ||||||
|  |     adjustTarget: ((Float) -> Float)?, | ||||||
|  |     block: (Animatable<Float, AnimationVector1D>.() -> Unit)? = null, | ||||||
|  | ): AnimationResult<Float, AnimationVector1D> { | ||||||
|  |     val targetValue = animationSpec.calculateTargetValue(value, initialVelocity) | ||||||
|  |     val adjustedTarget = adjustTarget?.invoke(targetValue) | ||||||
|  |  | ||||||
|  |     return if (adjustedTarget != null) { | ||||||
|  |         animateTo( | ||||||
|  |             targetValue = adjustedTarget, | ||||||
|  |             initialVelocity = initialVelocity, | ||||||
|  |             block = block | ||||||
|  |         ) | ||||||
|  |     } else { | ||||||
|  |         animateDecay( | ||||||
|  |             initialVelocity = initialVelocity, | ||||||
|  |             animationSpec = animationSpec, | ||||||
|  |             block = block, | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										219
									
								
								android/pickers/src/androidMain/kotlin/NumberPicker.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										219
									
								
								android/pickers/src/androidMain/kotlin/NumberPicker.kt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,219 @@ | |||||||
|  | package dev.inmo.micro_utils.android.pickers | ||||||
|  |  | ||||||
|  | import androidx.compose.animation.core.Animatable | ||||||
|  | import androidx.compose.animation.core.exponentialDecay | ||||||
|  | import androidx.compose.foundation.clickable | ||||||
|  | import androidx.compose.foundation.gestures.* | ||||||
|  | import androidx.compose.foundation.layout.* | ||||||
|  | import androidx.compose.foundation.text.KeyboardActions | ||||||
|  | import androidx.compose.foundation.text.KeyboardOptions | ||||||
|  | import androidx.compose.material.icons.Icons | ||||||
|  | import androidx.compose.material.icons.filled.KeyboardArrowDown | ||||||
|  | import androidx.compose.material.icons.filled.KeyboardArrowUp | ||||||
|  | import androidx.compose.material3.* | ||||||
|  | import androidx.compose.runtime.* | ||||||
|  | import androidx.compose.ui.Alignment | ||||||
|  | import androidx.compose.ui.ExperimentalComposeUiApi | ||||||
|  | import androidx.compose.ui.Modifier | ||||||
|  | import androidx.compose.ui.draw.alpha | ||||||
|  | import androidx.compose.ui.focus.FocusRequester | ||||||
|  | import androidx.compose.ui.focus.focusRequester | ||||||
|  | import androidx.compose.ui.geometry.Offset | ||||||
|  | import androidx.compose.ui.graphics.Color | ||||||
|  | import androidx.compose.ui.input.pointer.PointerInputScope | ||||||
|  | import androidx.compose.ui.input.pointer.pointerInput | ||||||
|  | import androidx.compose.ui.platform.LocalDensity | ||||||
|  | import androidx.compose.ui.text.ExperimentalTextApi | ||||||
|  | import androidx.compose.ui.text.TextStyle | ||||||
|  | import androidx.compose.ui.text.input.KeyboardType | ||||||
|  | import androidx.compose.ui.unit.IntOffset | ||||||
|  | import androidx.compose.ui.unit.center | ||||||
|  | import androidx.compose.ui.unit.dp | ||||||
|  | import dev.inmo.micro_utils.android.smalltextfield.SmallTextField | ||||||
|  | import kotlinx.coroutines.launch | ||||||
|  | import kotlin.math.abs | ||||||
|  | import kotlin.math.absoluteValue | ||||||
|  | import kotlin.math.roundToInt | ||||||
|  |  | ||||||
|  | private inline fun PointerInputScope.checkContains(offset: Offset): Boolean { | ||||||
|  |     return ((size.center.x - offset.x).absoluteValue < size.width / 2) && ((size.center.y - offset.y).absoluteValue < size.height / 2) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // src: https://gist.github.com/vganin/a9a84653a9f48a2d669910fbd48e32d5 | ||||||
|  |  | ||||||
|  | @OptIn(ExperimentalTextApi::class, ExperimentalComposeUiApi::class) | ||||||
|  | @Composable | ||||||
|  | fun NumberPicker( | ||||||
|  |     number: Int, | ||||||
|  |     modifier: Modifier = Modifier, | ||||||
|  |     range: IntRange? = null, | ||||||
|  |     textStyle: TextStyle = LocalTextStyle.current, | ||||||
|  |     arrowsColor: Color = MaterialTheme.colorScheme.primary, | ||||||
|  |     allowUseManualInput: Boolean = true, | ||||||
|  |     onStateChanged: (Int) -> Unit = {}, | ||||||
|  | ) { | ||||||
|  |     val coroutineScope = rememberCoroutineScope() | ||||||
|  |     val numbersColumnHeight = 36.dp | ||||||
|  |     val halvedNumbersColumnHeight = numbersColumnHeight / 2 | ||||||
|  |     val halvedNumbersColumnHeightPx = with(LocalDensity.current) { halvedNumbersColumnHeight.toPx() } | ||||||
|  |  | ||||||
|  |     fun animatedStateValue(offset: Float): Int = number - (offset / halvedNumbersColumnHeightPx).toInt() | ||||||
|  |  | ||||||
|  |     val animatedOffset = remember { Animatable(0f) }.apply { | ||||||
|  |         if (range != null) { | ||||||
|  |             val offsetRange = remember(number, range) { | ||||||
|  |                 val value = number | ||||||
|  |                 val first = -(range.last - value) * halvedNumbersColumnHeightPx | ||||||
|  |                 val last = -(range.first - value) * halvedNumbersColumnHeightPx | ||||||
|  |                 first..last | ||||||
|  |             } | ||||||
|  |             updateBounds(offsetRange.start, offsetRange.endInclusive) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     val coercedAnimatedOffset = animatedOffset.value % halvedNumbersColumnHeightPx | ||||||
|  |     val animatedStateValue = animatedStateValue(animatedOffset.value) | ||||||
|  |     val disabledArrowsColor = arrowsColor.copy(alpha = 0f) | ||||||
|  |  | ||||||
|  |     val inputFieldShown = if (allowUseManualInput) { | ||||||
|  |         remember { mutableStateOf(false) } | ||||||
|  |     } else { | ||||||
|  |         null | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     Column( | ||||||
|  |         modifier = modifier | ||||||
|  |             .wrapContentSize() | ||||||
|  |             .draggable( | ||||||
|  |                 orientation = Orientation.Vertical, | ||||||
|  |                 state = rememberDraggableState { deltaY -> | ||||||
|  |                     if (inputFieldShown ?.value != true) { | ||||||
|  |                         coroutineScope.launch { | ||||||
|  |                             animatedOffset.snapTo(animatedOffset.value + deltaY) | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 }, | ||||||
|  |                 onDragStopped = { velocity -> | ||||||
|  |                     if (inputFieldShown ?.value != true) { | ||||||
|  |                         coroutineScope.launch { | ||||||
|  |                             val endValue = animatedOffset.fling( | ||||||
|  |                                 initialVelocity = velocity, | ||||||
|  |                                 animationSpec = exponentialDecay(frictionMultiplier = 20f), | ||||||
|  |                                 adjustTarget = { target -> | ||||||
|  |                                     val coercedTarget = target % halvedNumbersColumnHeightPx | ||||||
|  |                                     val coercedAnchors = | ||||||
|  |                                         listOf(-halvedNumbersColumnHeightPx, 0f, halvedNumbersColumnHeightPx) | ||||||
|  |                                     val coercedPoint = coercedAnchors.minByOrNull { abs(it - coercedTarget) }!! | ||||||
|  |                                     val base = | ||||||
|  |                                         halvedNumbersColumnHeightPx * (target / halvedNumbersColumnHeightPx).toInt() | ||||||
|  |                                     coercedPoint + base | ||||||
|  |                                 } | ||||||
|  |                             ).endState.value | ||||||
|  |  | ||||||
|  |                             onStateChanged(animatedStateValue(endValue)) | ||||||
|  |                             animatedOffset.snapTo(0f) | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             ), | ||||||
|  |         horizontalAlignment = Alignment.CenterHorizontally | ||||||
|  |     ) { | ||||||
|  |         val spacing = 4.dp | ||||||
|  |  | ||||||
|  |         val upEnabled = range == null || range.first < number | ||||||
|  |         IconButton( | ||||||
|  |             { | ||||||
|  |                 onStateChanged(number - 1) | ||||||
|  |                 inputFieldShown ?.value = false | ||||||
|  |             }, | ||||||
|  |             enabled = upEnabled | ||||||
|  |         ) { | ||||||
|  |             Icon(Icons.Default.KeyboardArrowUp, "", tint = if (upEnabled) arrowsColor else disabledArrowsColor) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         Spacer(modifier = Modifier.height(spacing)) | ||||||
|  |         Box( | ||||||
|  |             modifier = Modifier | ||||||
|  |                 .offset { IntOffset(x = 0, y = coercedAnimatedOffset.roundToInt()) }, | ||||||
|  |             contentAlignment = Alignment.Center | ||||||
|  |         ) { | ||||||
|  |             val baseLabelModifier = Modifier.align(Alignment.Center) | ||||||
|  |             ProvideTextStyle(textStyle) { | ||||||
|  |                 Text( | ||||||
|  |                     text = (animatedStateValue - 1).toString(), | ||||||
|  |                     modifier = baseLabelModifier | ||||||
|  |                         .offset(y = -halvedNumbersColumnHeight) | ||||||
|  |                         .alpha(coercedAnimatedOffset / halvedNumbersColumnHeightPx) | ||||||
|  |                 ) | ||||||
|  |  | ||||||
|  |                 if (inputFieldShown ?.value == true) { | ||||||
|  |                     val currentValue = remember { mutableStateOf(number.toString()) } | ||||||
|  |  | ||||||
|  |                     val focusRequester = remember { FocusRequester() } | ||||||
|  |                     SmallTextField( | ||||||
|  |                         currentValue.value, | ||||||
|  |                         { | ||||||
|  |                             val asDigit = it.toIntOrNull() | ||||||
|  |                             when { | ||||||
|  |                                 (asDigit == null && it.isEmpty()) -> currentValue.value = (range ?.first ?: 0).toString() | ||||||
|  |                                 (asDigit != null && (range == null || asDigit in range)) -> currentValue.value = it | ||||||
|  |                                 else -> { /* do nothing */ } | ||||||
|  |                             } | ||||||
|  |                         }, | ||||||
|  |                         baseLabelModifier.focusRequester(focusRequester).width(IntrinsicSize.Min).pointerInput(number) { | ||||||
|  |                             detectTapGestures { | ||||||
|  |                                 if (!checkContains(it)) { | ||||||
|  |                                     currentValue.value.toIntOrNull() ?.let(onStateChanged) | ||||||
|  |                                     inputFieldShown.value = false | ||||||
|  |                                 } | ||||||
|  |                             } | ||||||
|  |                         }, | ||||||
|  |                         keyboardOptions = KeyboardOptions( | ||||||
|  |                             keyboardType = KeyboardType.Number | ||||||
|  |                         ), | ||||||
|  |                         keyboardActions = KeyboardActions { | ||||||
|  |                             currentValue.value.toIntOrNull() ?.let(onStateChanged) | ||||||
|  |                             inputFieldShown.value = false | ||||||
|  |                         }, | ||||||
|  |                         singleLine = true, | ||||||
|  |                         textStyle = textStyle | ||||||
|  |                     ) | ||||||
|  |                     LaunchedEffect(Unit) { | ||||||
|  |                         focusRequester.requestFocus() | ||||||
|  |                     } | ||||||
|  |                 } else { | ||||||
|  |                     Text( | ||||||
|  |                         text = animatedStateValue.toString(), | ||||||
|  |                         modifier = baseLabelModifier | ||||||
|  |                             .alpha(1 - abs(coercedAnimatedOffset) / halvedNumbersColumnHeightPx) | ||||||
|  |                             .clickable { | ||||||
|  |                                 if (inputFieldShown ?.value == false) { | ||||||
|  |                                     inputFieldShown.value = true | ||||||
|  |                                 } | ||||||
|  |                             } | ||||||
|  |                     ) | ||||||
|  |                 } | ||||||
|  |                 Text( | ||||||
|  |                     text = (animatedStateValue + 1).toString(), | ||||||
|  |                     modifier = baseLabelModifier | ||||||
|  |                         .offset(y = halvedNumbersColumnHeight) | ||||||
|  |                         .alpha(-coercedAnimatedOffset / halvedNumbersColumnHeightPx) | ||||||
|  |                 ) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |         Spacer(modifier = Modifier.height(spacing)) | ||||||
|  |  | ||||||
|  |         val downEnabled = range == null || range.last > number | ||||||
|  |         IconButton( | ||||||
|  |             { | ||||||
|  |                 onStateChanged(number + 1) | ||||||
|  |                 inputFieldShown ?.value = false | ||||||
|  |             }, | ||||||
|  |             enabled = downEnabled | ||||||
|  |         ) { | ||||||
|  |             Icon(Icons.Default.KeyboardArrowDown, "", tint = if (downEnabled) arrowsColor else disabledArrowsColor) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
							
								
								
									
										153
									
								
								android/pickers/src/androidMain/kotlin/SetPicker.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										153
									
								
								android/pickers/src/androidMain/kotlin/SetPicker.kt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,153 @@ | |||||||
|  | package dev.inmo.micro_utils.android.pickers | ||||||
|  |  | ||||||
|  | import androidx.compose.animation.core.Animatable | ||||||
|  | import androidx.compose.animation.core.exponentialDecay | ||||||
|  | import androidx.compose.foundation.gestures.* | ||||||
|  | import androidx.compose.foundation.layout.* | ||||||
|  | import androidx.compose.foundation.rememberScrollState | ||||||
|  | import androidx.compose.material.icons.Icons | ||||||
|  | import androidx.compose.material.icons.filled.KeyboardArrowDown | ||||||
|  | import androidx.compose.material.icons.filled.KeyboardArrowUp | ||||||
|  | import androidx.compose.material3.* | ||||||
|  | import androidx.compose.runtime.* | ||||||
|  | import androidx.compose.ui.Alignment | ||||||
|  | import androidx.compose.ui.ExperimentalComposeUiApi | ||||||
|  | import androidx.compose.ui.Modifier | ||||||
|  | import androidx.compose.ui.draw.alpha | ||||||
|  | import androidx.compose.ui.graphics.Color | ||||||
|  | import androidx.compose.ui.platform.LocalDensity | ||||||
|  | import androidx.compose.ui.text.ExperimentalTextApi | ||||||
|  | import androidx.compose.ui.text.TextStyle | ||||||
|  | import androidx.compose.ui.unit.dp | ||||||
|  | import kotlinx.coroutines.launch | ||||||
|  | import kotlin.math.* | ||||||
|  |  | ||||||
|  | @OptIn(ExperimentalTextApi::class, ExperimentalComposeUiApi::class) | ||||||
|  | @Composable | ||||||
|  | fun <T> SetPicker( | ||||||
|  |     current: T, | ||||||
|  |     dataList: List<T>, | ||||||
|  |     modifier: Modifier = Modifier, | ||||||
|  |     textStyle: TextStyle = LocalTextStyle.current, | ||||||
|  |     arrowsColor: Color = MaterialTheme.colorScheme.primary, | ||||||
|  |     dataToString: @Composable (T) -> String = { it.toString() }, | ||||||
|  |     onStateChanged: (T) -> Unit = {}, | ||||||
|  | ) { | ||||||
|  |     val coroutineScope = rememberCoroutineScope() | ||||||
|  |     val numbersColumnHeight = 8.dp + with(LocalDensity.current) { | ||||||
|  |         textStyle.lineHeight.toDp() | ||||||
|  |     } | ||||||
|  |     val numbersColumnHeightPx = with(LocalDensity.current) { numbersColumnHeight.toPx() } | ||||||
|  |     val halvedNumbersColumnHeight = numbersColumnHeight / 2 | ||||||
|  |     val halvedNumbersColumnHeightPx = with(LocalDensity.current) { halvedNumbersColumnHeight.toPx() } | ||||||
|  |  | ||||||
|  |     val index = dataList.indexOfFirst { it === current }.takeIf { it > -1 } ?: dataList.indexOf(current) | ||||||
|  |     val lastIndex = dataList.size - 1 | ||||||
|  |  | ||||||
|  |     fun animatedStateValue(offset: Float): Int = index - (offset / halvedNumbersColumnHeightPx).toInt() | ||||||
|  |  | ||||||
|  |     val animatedOffset = remember { Animatable(0f) }.apply { | ||||||
|  |         val offsetRange = remember(index, lastIndex) { | ||||||
|  |             val value = index | ||||||
|  |             val first = -(lastIndex - value) * halvedNumbersColumnHeightPx | ||||||
|  |             val last = value * halvedNumbersColumnHeightPx | ||||||
|  |             first..last | ||||||
|  |         } | ||||||
|  |         updateBounds(offsetRange.start, offsetRange.endInclusive) | ||||||
|  |     } | ||||||
|  |     val indexAnimatedOffset = if (animatedOffset.value > 0) { | ||||||
|  |         (index - floor(animatedOffset.value / halvedNumbersColumnHeightPx).toInt()) | ||||||
|  |     } else { | ||||||
|  |         (index - ceil(animatedOffset.value / halvedNumbersColumnHeightPx).toInt()) | ||||||
|  |     } | ||||||
|  |     val coercedAnimatedOffset = animatedOffset.value % halvedNumbersColumnHeightPx | ||||||
|  |     val disabledArrowsColor = arrowsColor.copy(alpha = 0f) | ||||||
|  |  | ||||||
|  |     Column( | ||||||
|  |         modifier = modifier | ||||||
|  |             .wrapContentSize() | ||||||
|  |             .draggable( | ||||||
|  |                 orientation = Orientation.Vertical, | ||||||
|  |                 state = rememberDraggableState { deltaY -> | ||||||
|  |                     coroutineScope.launch { | ||||||
|  |                         animatedOffset.snapTo(animatedOffset.value + deltaY) | ||||||
|  |                     } | ||||||
|  |                 }, | ||||||
|  |                 onDragStopped = { velocity -> | ||||||
|  |                     coroutineScope.launch { | ||||||
|  |                         val endValue = animatedOffset.fling( | ||||||
|  |                             initialVelocity = velocity, | ||||||
|  |                             animationSpec = exponentialDecay(frictionMultiplier = 20f), | ||||||
|  |                             adjustTarget = { target -> | ||||||
|  |                                 val coercedTarget = target % halvedNumbersColumnHeightPx | ||||||
|  |                                 val coercedAnchors = | ||||||
|  |                                     listOf(-halvedNumbersColumnHeightPx, 0f, halvedNumbersColumnHeightPx) | ||||||
|  |                                 val coercedPoint = coercedAnchors.minByOrNull { abs(it - coercedTarget) }!! | ||||||
|  |                                 val base = | ||||||
|  |                                     halvedNumbersColumnHeightPx * (target / halvedNumbersColumnHeightPx).toInt() | ||||||
|  |                                 coercedPoint + base | ||||||
|  |                             } | ||||||
|  |                         ).endState.value | ||||||
|  |  | ||||||
|  |                         onStateChanged(dataList.elementAt(animatedStateValue(endValue))) | ||||||
|  |                         animatedOffset.snapTo(0f) | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             ), | ||||||
|  |         horizontalAlignment = Alignment.CenterHorizontally | ||||||
|  |     ) { | ||||||
|  |         val spacing = 4.dp | ||||||
|  |  | ||||||
|  |         val upEnabled = index > 0 | ||||||
|  |         IconButton( | ||||||
|  |             { | ||||||
|  |                 onStateChanged(dataList.elementAt(index - 1)) | ||||||
|  |             }, | ||||||
|  |             enabled = upEnabled | ||||||
|  |         ) { | ||||||
|  |             Icon(Icons.Default.KeyboardArrowUp, "", tint = if (upEnabled) arrowsColor else disabledArrowsColor) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         Spacer(modifier = Modifier.height(spacing)) | ||||||
|  |         Box( | ||||||
|  |             modifier = Modifier, | ||||||
|  |             contentAlignment = Alignment.Center | ||||||
|  |         ) { | ||||||
|  |             ProvideTextStyle(textStyle) { | ||||||
|  |                 dataList.forEachIndexed { i, t -> | ||||||
|  |                     val alpha = when { | ||||||
|  |                         i == indexAnimatedOffset - 1 -> coercedAnimatedOffset / halvedNumbersColumnHeightPx | ||||||
|  |                         i == indexAnimatedOffset -> 1 - (abs(coercedAnimatedOffset) / halvedNumbersColumnHeightPx) | ||||||
|  |                         i == indexAnimatedOffset + 1 -> -coercedAnimatedOffset / halvedNumbersColumnHeightPx | ||||||
|  |                         else -> return@forEachIndexed | ||||||
|  |                     } | ||||||
|  |                     val offset = when { | ||||||
|  |                         i == indexAnimatedOffset - 1 && coercedAnimatedOffset > 0 -> coercedAnimatedOffset - halvedNumbersColumnHeightPx | ||||||
|  |                         i == indexAnimatedOffset -> coercedAnimatedOffset | ||||||
|  |                         i == indexAnimatedOffset + 1 && coercedAnimatedOffset < 0 -> coercedAnimatedOffset + halvedNumbersColumnHeightPx | ||||||
|  |                         else -> return@forEachIndexed | ||||||
|  |                     } | ||||||
|  |                     Text( | ||||||
|  |                         text = dataToString(t), | ||||||
|  |                         modifier = Modifier | ||||||
|  |                             .alpha(alpha) | ||||||
|  |                             .offset(y = with(LocalDensity.current) { offset.toDp() }) | ||||||
|  |                     ) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |         Spacer(modifier = Modifier.height(spacing)) | ||||||
|  |  | ||||||
|  |         val downEnabled = index < lastIndex | ||||||
|  |         IconButton( | ||||||
|  |             { | ||||||
|  |                 onStateChanged(dataList.elementAt(index + 1)) | ||||||
|  |             }, | ||||||
|  |             enabled = downEnabled | ||||||
|  |         ) { | ||||||
|  |             Icon(Icons.Default.KeyboardArrowDown, "", tint = if (downEnabled) arrowsColor else disabledArrowsColor) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										23
									
								
								android/recyclerview/build.gradle
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								android/recyclerview/build.gradle
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | |||||||
|  | plugins { | ||||||
|  |     id "org.jetbrains.kotlin.multiplatform" | ||||||
|  |     id "org.jetbrains.kotlin.plugin.serialization" | ||||||
|  |     id "com.android.library" | ||||||
|  | } | ||||||
|  |  | ||||||
|  | apply from: "$mppAndroidProject" | ||||||
|  |  | ||||||
|  | kotlin { | ||||||
|  |     sourceSets { | ||||||
|  |         commonMain { | ||||||
|  |             dependencies { | ||||||
|  |                 api libs.kt.coroutines | ||||||
|  |                 api project(":micro_utils.common") | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         androidMain { | ||||||
|  |             dependencies { | ||||||
|  |                 api libs.android.recyclerView | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										1
									
								
								android/recyclerview/src/androidMain/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								android/recyclerview/src/androidMain/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | <manifest/> | ||||||
| @@ -0,0 +1,22 @@ | |||||||
|  | package dev.inmo.micro_utils.android.recyclerview | ||||||
|  |  | ||||||
|  | import android.view.LayoutInflater | ||||||
|  | import android.view.View | ||||||
|  | import android.view.ViewGroup | ||||||
|  |  | ||||||
|  | abstract class AbstractStandardViewHolder<T>( | ||||||
|  |     inflater: LayoutInflater, | ||||||
|  |     container: ViewGroup?, | ||||||
|  |     viewId: Int, | ||||||
|  |     onViewInflated: ((View) -> Unit)? = null | ||||||
|  | ) : AbstractViewHolder<T>( | ||||||
|  |     inflater.inflate(viewId, container, false).also { | ||||||
|  |         onViewInflated ?.invoke(it) | ||||||
|  |     } | ||||||
|  | ) { | ||||||
|  |     constructor( | ||||||
|  |         container: ViewGroup, | ||||||
|  |         viewId: Int, | ||||||
|  |         onViewInflated: ((View) -> Unit)? = null | ||||||
|  |     ) : this(LayoutInflater.from(container.context), container, viewId, onViewInflated) | ||||||
|  | } | ||||||
| @@ -0,0 +1,10 @@ | |||||||
|  | package dev.inmo.micro_utils.android.recyclerview | ||||||
|  |  | ||||||
|  | import android.view.View | ||||||
|  | import androidx.recyclerview.widget.RecyclerView | ||||||
|  |  | ||||||
|  | abstract class AbstractViewHolder<in T>( | ||||||
|  |     view: View | ||||||
|  | ) : RecyclerView.ViewHolder(view) { | ||||||
|  |     abstract fun onBind(item: T) | ||||||
|  | } | ||||||
| @@ -0,0 +1,9 @@ | |||||||
|  | package dev.inmo.micro_utils.android.recyclerview | ||||||
|  |  | ||||||
|  | import android.content.Context | ||||||
|  | import android.widget.LinearLayout | ||||||
|  | import androidx.recyclerview.widget.DividerItemDecoration | ||||||
|  |  | ||||||
|  | val Context.recyclerViewItemsDecoration | ||||||
|  |     get() = DividerItemDecoration(this, LinearLayout.VERTICAL) | ||||||
|  |  | ||||||
| @@ -0,0 +1,53 @@ | |||||||
|  | package dev.inmo.micro_utils.android.recyclerview | ||||||
|  |  | ||||||
|  | import androidx.recyclerview.widget.* | ||||||
|  | import kotlinx.coroutines.CoroutineScope | ||||||
|  | import kotlinx.coroutines.flow.* | ||||||
|  |  | ||||||
|  | @Suppress("NOTHING_TO_INLINE") | ||||||
|  | private inline fun RecyclerView.LayoutManager.findLastVisibleItemPositionGetter(): (() -> Int)? = when (this) { | ||||||
|  |     is LinearLayoutManager -> ::findLastVisibleItemPosition | ||||||
|  |     is GridLayoutManager -> ::findLastVisibleItemPosition | ||||||
|  |     else -> null | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fun RecyclerView.lastVisibleItemFlow( | ||||||
|  |     completingScope: CoroutineScope | ||||||
|  | ): Flow<Int> { | ||||||
|  |     val lastVisibleElementFun: () -> Int = layoutManager ?.findLastVisibleItemPositionGetter() ?: error("Currently supported only linear and grid layout manager") | ||||||
|  |     val lastVisibleFlow = MutableStateFlow(lastVisibleElementFun()) | ||||||
|  |     addOnScrollListener( | ||||||
|  |         object : RecyclerView.OnScrollListener() { | ||||||
|  |             override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { | ||||||
|  |                 super.onScrolled(recyclerView, dx, dy) | ||||||
|  |                 lastVisibleFlow.value = lastVisibleElementFun() | ||||||
|  |             } | ||||||
|  |         }.also { scrollListener -> | ||||||
|  |             lastVisibleFlow.onCompletion { | ||||||
|  |                 removeOnScrollListener(scrollListener) | ||||||
|  |             }.launchIn(completingScope) | ||||||
|  |         } | ||||||
|  |     ) | ||||||
|  |     return lastVisibleFlow.asStateFlow() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | inline fun Flow<Int>.mapLeftItems( | ||||||
|  |     crossinline countGetter: () -> Int | ||||||
|  | ): Flow<Int> = map { countGetter() - it } | ||||||
|  |  | ||||||
|  | inline fun Flow<Int>.mapRequireFilling( | ||||||
|  |     minimalLeftItems: Int, | ||||||
|  |     crossinline countGetter: () -> Int | ||||||
|  | ): Flow<Int> = mapLeftItems(countGetter).mapNotNull { | ||||||
|  |     if (it < minimalLeftItems) { | ||||||
|  |         it | ||||||
|  |     } else { | ||||||
|  |         null | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | inline fun RecyclerView.mapRequireFilling( | ||||||
|  |     minimalLeftItems: Int, | ||||||
|  |     completingScope: CoroutineScope, | ||||||
|  |     crossinline countGetter: () -> Int | ||||||
|  | ): Flow<Int> = lastVisibleItemFlow(completingScope).mapRequireFilling(minimalLeftItems, countGetter) | ||||||
| @@ -0,0 +1,91 @@ | |||||||
|  | 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>: 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 | ||||||
|  |             checkEmpty() | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |     init { | ||||||
|  |         registerAdapterDataObserver( | ||||||
|  |             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() | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         ) | ||||||
|  |         checkEmpty() | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun getItemCount(): Int = data.size | ||||||
|  |  | ||||||
|  |     override fun onBindViewHolder(holder: AbstractViewHolder<T>, position: Int) { | ||||||
|  |         holder.onBind(data[position]) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private fun checkEmpty() { | ||||||
|  |         emptyView ?. let { | ||||||
|  |             if (dataCountState.value == 0) { | ||||||
|  |                 it.visibility = View.VISIBLE | ||||||
|  |             } else { | ||||||
|  |                 it.visibility = View.GONE | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | 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) | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										19
									
								
								android/smalltextfield/build.gradle
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								android/smalltextfield/build.gradle
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | |||||||
|  | plugins { | ||||||
|  |     id "org.jetbrains.kotlin.multiplatform" | ||||||
|  |     id "org.jetbrains.kotlin.plugin.serialization" | ||||||
|  |     id "com.android.library" | ||||||
|  |     alias(libs.plugins.jb.compose) | ||||||
|  |     alias(libs.plugins.kt.jb.compose) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | apply from: "$mppProjectWithSerializationAndCompose" | ||||||
|  |  | ||||||
|  | kotlin { | ||||||
|  |     sourceSets { | ||||||
|  |         androidMain { | ||||||
|  |             dependencies { | ||||||
|  |                 api libs.jb.compose.material3 | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1 @@ | |||||||
|  | <manifest/> | ||||||
| @@ -0,0 +1,66 @@ | |||||||
|  | package dev.inmo.micro_utils.android.smalltextfield | ||||||
|  |  | ||||||
|  | import androidx.compose.foundation.interaction.MutableInteractionSource | ||||||
|  | import androidx.compose.foundation.layout.Box | ||||||
|  | import androidx.compose.foundation.layout.Row | ||||||
|  | import androidx.compose.foundation.layout.defaultMinSize | ||||||
|  | import androidx.compose.foundation.text.BasicTextField | ||||||
|  | import androidx.compose.foundation.text.KeyboardActions | ||||||
|  | import androidx.compose.foundation.text.KeyboardOptions | ||||||
|  | import androidx.compose.foundation.text.selection.LocalTextSelectionColors | ||||||
|  | import androidx.compose.material3.* | ||||||
|  | import androidx.compose.runtime.Composable | ||||||
|  | import androidx.compose.runtime.CompositionLocalProvider | ||||||
|  | import androidx.compose.runtime.remember | ||||||
|  | import androidx.compose.ui.Alignment | ||||||
|  | import androidx.compose.ui.Modifier | ||||||
|  | import androidx.compose.ui.graphics.Color | ||||||
|  | import androidx.compose.ui.graphics.Shape | ||||||
|  | import androidx.compose.ui.graphics.SolidColor | ||||||
|  | import androidx.compose.ui.graphics.takeOrElse | ||||||
|  | import androidx.compose.ui.text.TextStyle | ||||||
|  | import androidx.compose.ui.text.input.VisualTransformation | ||||||
|  |  | ||||||
|  | @OptIn(ExperimentalMaterial3Api::class) | ||||||
|  | @Composable | ||||||
|  | fun SmallTextField( | ||||||
|  |     value: String, | ||||||
|  |     onValueChange: (String) -> Unit, | ||||||
|  |     modifier: Modifier = Modifier, | ||||||
|  |     enabled: Boolean = true, | ||||||
|  |     readOnly: Boolean = false, | ||||||
|  |     textStyle: TextStyle = LocalTextStyle.current, | ||||||
|  |     textColor: Color = textStyle.color.takeOrElse { | ||||||
|  |         LocalContentColor.current | ||||||
|  |     }, | ||||||
|  |     visualTransformation: VisualTransformation = VisualTransformation.None, | ||||||
|  |     keyboardOptions: KeyboardOptions = KeyboardOptions.Default, | ||||||
|  |     keyboardActions: KeyboardActions = KeyboardActions.Default, | ||||||
|  |     singleLine: Boolean = false, | ||||||
|  |     maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE, | ||||||
|  |     minLines: Int = 1, | ||||||
|  |     interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, | ||||||
|  | ) { | ||||||
|  |     BasicTextField( | ||||||
|  |         value = value, | ||||||
|  |         modifier = modifier, | ||||||
|  |         onValueChange = onValueChange, | ||||||
|  |         enabled = enabled, | ||||||
|  |         readOnly = readOnly, | ||||||
|  |         textStyle = textStyle.copy( | ||||||
|  |             color = textColor | ||||||
|  |         ), | ||||||
|  |         visualTransformation = visualTransformation, | ||||||
|  |         keyboardOptions = keyboardOptions, | ||||||
|  |         keyboardActions = keyboardActions, | ||||||
|  |         interactionSource = interactionSource, | ||||||
|  |         singleLine = singleLine, | ||||||
|  |         maxLines = maxLines, | ||||||
|  |         minLines = minLines, | ||||||
|  |         cursorBrush = SolidColor( | ||||||
|  |             textStyle.color.takeOrElse { | ||||||
|  |                 LocalContentColor.current | ||||||
|  |             } | ||||||
|  |         ) | ||||||
|  |     ) | ||||||
|  | } | ||||||
							
								
								
									
										42
									
								
								build.gradle
									
									
									
									
									
								
							
							
						
						
									
										42
									
								
								build.gradle
									
									
									
									
									
								
							| @@ -1,27 +1,51 @@ | |||||||
| buildscript { | buildscript { | ||||||
|     repositories { |     repositories { | ||||||
|         mavenLocal() |         google() | ||||||
|         jcenter() |  | ||||||
|         mavenCentral() |         mavenCentral() | ||||||
|  |         mavenLocal() | ||||||
|         maven { url "https://plugins.gradle.org/m2/" } |         maven { url "https://plugins.gradle.org/m2/" } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     dependencies { |     dependencies { | ||||||
|         classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" |         classpath libs.buildscript.kt.gradle | ||||||
|         classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" |         classpath libs.buildscript.kt.serialization | ||||||
|         classpath "com.jfrog.bintray.gradle:gradle-bintray-plugin:$gradle_bintray_plugin_version" |         classpath libs.buildscript.kt.ksp | ||||||
|         classpath "com.github.breadmoirai:github-release:$github_release_plugin_version" |         classpath libs.buildscript.jb.dokka | ||||||
|  |         classpath libs.buildscript.gh.release | ||||||
|  |         classpath libs.buildscript.android.gradle | ||||||
|  |         classpath libs.buildscript.android.dexcount | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | plugins { | ||||||
|  |     alias(libs.plugins.versions) | ||||||
|  |     alias(libs.plugins.nmcp.aggregation) | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | if ((project.hasProperty('SONATYPE_USER') || System.getenv('SONATYPE_USER') != null) && (project.hasProperty('SONATYPE_PASSWORD') || System.getenv('SONATYPE_PASSWORD') != null)) { | ||||||
|  |     nmcpAggregation { | ||||||
|  |         centralPortal { | ||||||
|  |             username = project.hasProperty('SONATYPE_USER') ? project.property('SONATYPE_USER') : System.getenv('SONATYPE_USER') | ||||||
|  |             password = project.hasProperty('SONATYPE_PASSWORD') ? project.property('SONATYPE_PASSWORD') : System.getenv('SONATYPE_PASSWORD') | ||||||
|  |             validationTimeout = Duration.ofHours(4) | ||||||
|  |             publishingType = System.getenv('PUBLISHING_TYPE') != "" ? System.getenv('PUBLISHING_TYPE') : "USER_MANAGED" | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         publishAllProjectsProbablyBreakingProjectIsolation() | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| allprojects { | allprojects { | ||||||
|     repositories { |     repositories { | ||||||
|         mavenLocal() |  | ||||||
|         jcenter() |  | ||||||
|         mavenCentral() |         mavenCentral() | ||||||
|         maven { url "https://kotlin.bintray.com/kotlinx" } |         google() | ||||||
|  |         maven { url "https://maven.pkg.jetbrains.space/public/p/compose/dev" } | ||||||
|  |         maven { url "https://nexus.inmo.dev/repository/maven-releases/" } | ||||||
|  |         mavenLocal() | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| apply from: "./extensions.gradle" | apply from: "./extensions.gradle" | ||||||
| apply from: "./github_release.gradle" | apply from: "./github_release.gradle" | ||||||
|  | apply from: "./versions_plugin_setup.gradle" | ||||||
|   | |||||||
| @@ -12,4 +12,7 @@ function assert_success() { | |||||||
| export RELEASE_MODE=true | export RELEASE_MODE=true | ||||||
| project="$1" | project="$1" | ||||||
|  |  | ||||||
| assert_success ./gradlew clean "$project:clean" "$project:build" "$project:publishToMavenLocal" "$project:bintrayUpload" | assert_success ./gradlew clean | ||||||
|  | assert_success ./gradlew "$project:build" | ||||||
|  | assert_success ./gradlew "$project:publishToMavenLocal" | ||||||
|  | assert_success ./gradlew "$project:bintrayUpload" | ||||||
|   | |||||||
							
								
								
									
										17
									
								
								colors/build.gradle
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								colors/build.gradle
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | |||||||
|  | plugins { | ||||||
|  |     id "org.jetbrains.kotlin.multiplatform" | ||||||
|  |     id "org.jetbrains.kotlin.plugin.serialization" | ||||||
|  |     id "com.android.library" | ||||||
|  | } | ||||||
|  |  | ||||||
|  | apply from: "$mppJvmJsWasmJsAndroidLinuxMingwLinuxArm64Project" | ||||||
|  |  | ||||||
|  | kotlin { | ||||||
|  |     sourceSets { | ||||||
|  |         commonMain { | ||||||
|  |             dependencies { | ||||||
|  |                 api project(":micro_utils.colors.common") | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										7
									
								
								colors/common/build.gradle
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								colors/common/build.gradle
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | |||||||
|  | plugins { | ||||||
|  |     id "org.jetbrains.kotlin.multiplatform" | ||||||
|  |     id "org.jetbrains.kotlin.plugin.serialization" | ||||||
|  |     id "com.android.library" | ||||||
|  | } | ||||||
|  |  | ||||||
|  | apply from: "$mppJvmJsWasmJsAndroidLinuxMingwLinuxArm64Project" | ||||||
							
								
								
									
										174
									
								
								colors/common/src/commonMain/kotlin/HEXAColor.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										174
									
								
								colors/common/src/commonMain/kotlin/HEXAColor.kt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,174 @@ | |||||||
|  | package dev.inmo.micro_utils.colors.common | ||||||
|  |  | ||||||
|  | import kotlinx.serialization.Serializable | ||||||
|  | import kotlin.jvm.JvmInline | ||||||
|  | import kotlin.math.floor | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Wrapper for RGBA colors. Receiving [UInt] in main constructor. Each part in main constructor | ||||||
|  |  * configured with `0x00 - 0xff` range. Examples: | ||||||
|  |  * | ||||||
|  |  * * Red: `0xff0000ffu` | ||||||
|  |  * * Red (0.5 capacity): `0xff000088u` | ||||||
|  |  * | ||||||
|  |  * Anyway it is recommended to use | ||||||
|  |  * | ||||||
|  |  * @param hexaUInt rgba [UInt] in format `0xRRGGBBAA` where RR - red, GG - green, BB - blue` and AA - alpha | ||||||
|  |  */ | ||||||
|  | @Serializable | ||||||
|  | @JvmInline | ||||||
|  | value class HEXAColor ( | ||||||
|  |     val hexaUInt: UInt | ||||||
|  | ) : Comparable<HEXAColor> { | ||||||
|  |     /** | ||||||
|  |      * @returns [hexaUInt] as a string with format `#RRGGBBAA` where RR - red, GG - green, BB - blue and AA - alpha | ||||||
|  |      */ | ||||||
|  |     val hexa: String | ||||||
|  |         get() = "#${hexaUInt.toString(16).padStart(8, '0')}" | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * @returns [hexaUInt] as a string with format `#RRGGBB` where RR - red, GG - green and BB - blue | ||||||
|  |      */ | ||||||
|  |     val hex: String | ||||||
|  |         get() = hexa.take(7) | ||||||
|  |     /** | ||||||
|  |      * @returns [hexaUInt] as a string with format `#AARRGGBB` where AA - alpha, RR - red, GG - green and BB - blue | ||||||
|  |      */ | ||||||
|  |     val ahex: String | ||||||
|  |         get() = "#${a.toString(16).padStart(2, '2')}${hex.drop(1)}" | ||||||
|  |     val rgba: String | ||||||
|  |         get() = "rgba($r,$g,$b,${aOfOne.toString().take(5)})" | ||||||
|  |     val rgb: String | ||||||
|  |         get() = "rgb($r,$g,$b)" | ||||||
|  |     val shortHex: String | ||||||
|  |         get() = "#${r.shortPart()}${g.shortPart()}${b.shortPart()}" | ||||||
|  |     val shortHexa: String | ||||||
|  |         get() = "$shortHex${a.shortPart()}" | ||||||
|  |     val rgbUInt: UInt | ||||||
|  |         get() = (hexaUInt / 256u) | ||||||
|  |     val rgbInt: Int | ||||||
|  |         get() = rgbUInt.toInt() | ||||||
|  |     val ahexUInt | ||||||
|  |         get() = (a * 0x1000000).toUInt() + rgbUInt | ||||||
|  |  | ||||||
|  |     val r: Int | ||||||
|  |         get() = ((hexaUInt and 0xff000000u) / 0x1000000u).toInt() | ||||||
|  |     val g: Int | ||||||
|  |         get() = ((hexaUInt and 0x00ff0000u) / 0x10000u).toInt() | ||||||
|  |     val b: Int | ||||||
|  |         get() = ((hexaUInt and 0x0000ff00u) / 0x100u).toInt() | ||||||
|  |     val a: Int | ||||||
|  |         get() = ((hexaUInt and 0x000000ffu)).toInt() | ||||||
|  |     val aOfOne: Float | ||||||
|  |         get() = a.toFloat() / (0xff) | ||||||
|  |     init { | ||||||
|  |         require(hexaUInt in 0u ..0xffffffffu) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     constructor(r: Int, g: Int, b: Int, a: Int) : this( | ||||||
|  |         ((r * 0x1000000).toLong() + g * 0x10000 + b * 0x100 + a).toUInt() | ||||||
|  |     ) { | ||||||
|  |         require(r in 0 ..0xff) | ||||||
|  |         require(g in 0 ..0xff) | ||||||
|  |         require(b in 0 ..0xff) | ||||||
|  |         require(a in 0 ..0xff) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     constructor(r: Int, g: Int, b: Int, aOfOne: Float = 1f) : this( | ||||||
|  |         r = r, g = g, b = b, a = (aOfOne * 0xff).toInt() | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     override fun toString(): String { | ||||||
|  |         return hexa | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun compareTo(other: HEXAColor): Int = (hexaUInt - other.hexaUInt).coerceIn(Int.MIN_VALUE.toUInt(), Int.MAX_VALUE.toLong().toUInt()).toInt() | ||||||
|  |  | ||||||
|  |     fun copy( | ||||||
|  |         r: Int = this.r, | ||||||
|  |         g: Int = this.g, | ||||||
|  |         b: Int = this.b, | ||||||
|  |         aOfOne: Float = this.aOfOne | ||||||
|  |     ) = HEXAColor(r = r, g = g, b = b, aOfOne = aOfOne) | ||||||
|  |     fun copy( | ||||||
|  |         r: Int = this.r, | ||||||
|  |         g: Int = this.g, | ||||||
|  |         b: Int = this.b, | ||||||
|  |         a: Int | ||||||
|  |     ) = HEXAColor(r = r, g = g, b = b, a = a) | ||||||
|  |  | ||||||
|  |     companion object { | ||||||
|  |         /** | ||||||
|  |          * Parsing color from [color] | ||||||
|  |          * | ||||||
|  |          * Supported formats samples (on Red color based): | ||||||
|  |          * | ||||||
|  |          * * `#f00` | ||||||
|  |          * * `#f00f` | ||||||
|  |          * * `#ff0000` | ||||||
|  |          * * `#ff0000ff` | ||||||
|  |          * * `rgb(255, 0, 0)` | ||||||
|  |          * * `rgba(255, 0, 0, 1)` | ||||||
|  |          */ | ||||||
|  |         fun parseStringColor(color: String): HEXAColor = when { | ||||||
|  |             color.startsWith("#") -> color.removePrefix("#").let { color -> | ||||||
|  |                 when (color.length) { | ||||||
|  |                     3 -> color.map { "$it$it" }.joinToString(separator = "", postfix = "ff") | ||||||
|  |                     4 -> color.take(3).map { "$it$it" }.joinToString(separator = "", postfix = color.takeLast(1).let { "${it}0" }) | ||||||
|  |                     6 -> "${color}ff" | ||||||
|  |                     8 -> color | ||||||
|  |                     else -> error("Malfurmed color string: $color. It is expected that color started with # will contains 3, 6 or 8 valuable parts") | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             color.startsWith("rgb(") -> color | ||||||
|  |                 .removePrefix("rgb(") | ||||||
|  |                 .removeSuffix(")") | ||||||
|  |                 .replace(Regex("\\s"), "") | ||||||
|  |                 .split(",") | ||||||
|  |                 .joinToString("", postfix = "ff") { | ||||||
|  |                     it.toInt().toString(16).padStart(2, '0') | ||||||
|  |                 } | ||||||
|  |             color.startsWith("rgba(") -> color | ||||||
|  |                 .removePrefix("rgba(") | ||||||
|  |                 .removeSuffix(")") | ||||||
|  |                 .replace(Regex("\\s"), "") | ||||||
|  |                 .split(",").let { | ||||||
|  |                     it.take(3).map { it.toInt().toString(16).padStart(2, '0') } + (it.last().toFloat() * 0xff).toInt().toString(16).padStart(2, '0') | ||||||
|  |                 } | ||||||
|  |                 .joinToString("") | ||||||
|  |             else -> color | ||||||
|  |         }.lowercase().toUInt(16).let(::HEXAColor) | ||||||
|  |  | ||||||
|  |         /** | ||||||
|  |          * Creates [HEXAColor] from [uint] presume it is in format `0xRRGGBBAA` where RR - red, GG - green, BB - blue` and AA - alpha | ||||||
|  |          */ | ||||||
|  |         fun fromHexa(uint: UInt) = HEXAColor(uint) | ||||||
|  |  | ||||||
|  |         /** | ||||||
|  |          * Creates [HEXAColor] from [uint] presume it is in format `0xAARRGGBB` where AA - alpha, RR - red, GG - green and BB - blue` | ||||||
|  |          */ | ||||||
|  |         fun fromAhex(uint: UInt) = HEXAColor( | ||||||
|  |             a = ((uint and 0xff000000u) / 0x1000000u).toInt(), | ||||||
|  |             r = ((uint and 0x00ff0000u) / 0x10000u).toInt(), | ||||||
|  |             g = ((uint and 0x0000ff00u) / 0x100u).toInt(), | ||||||
|  |             b = ((uint and 0x000000ffu)).toInt() | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         /** | ||||||
|  |          * Parsing color from [color] | ||||||
|  |          * | ||||||
|  |          * Supported formats samples (on Red color based): | ||||||
|  |          * | ||||||
|  |          * * `#f00` | ||||||
|  |          * * `#ff0000` | ||||||
|  |          * * `#ff0000ff` | ||||||
|  |          * * `rgb(255, 0, 0)` | ||||||
|  |          * * `rgba(255, 0, 0, 1)` | ||||||
|  |          */ | ||||||
|  |         operator fun invoke(color: String) = parseStringColor(color) | ||||||
|  |  | ||||||
|  |         private fun Int.shortPart(): String { | ||||||
|  |             return (floor(toFloat() / 16)).toInt().toString(16) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										209
									
								
								colors/common/src/commonTest/kotlin/HexColorTests.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										209
									
								
								colors/common/src/commonTest/kotlin/HexColorTests.kt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,209 @@ | |||||||
|  | package dev.inmo.micro_utils.colors.common | ||||||
|  |  | ||||||
|  | import kotlin.math.floor | ||||||
|  | import kotlin.test.Test | ||||||
|  | import kotlin.test.assertEquals | ||||||
|  | import kotlin.test.assertTrue | ||||||
|  |  | ||||||
|  | class HexColorTests { | ||||||
|  |     val alphaRgbaPrecision = 5 | ||||||
|  |     class TestColor( | ||||||
|  |         val color: HEXAColor, | ||||||
|  |         val shortHex: String, | ||||||
|  |         val shortHexa: String, | ||||||
|  |         val hex: String, | ||||||
|  |         val hexa: String, | ||||||
|  |         val ahex: String, | ||||||
|  |         val ahexUInt: UInt, | ||||||
|  |         val rgbUInt: UInt, | ||||||
|  |         val rgb: String, | ||||||
|  |         val rgba: String, | ||||||
|  |         val r: Int, | ||||||
|  |         val g: Int, | ||||||
|  |         val b: Int, | ||||||
|  |         val a: Int, | ||||||
|  |         vararg val additionalRGBAVariants: String | ||||||
|  |     ) | ||||||
|  |     val testColors: List<TestColor> | ||||||
|  |         get() = listOf( | ||||||
|  |             TestColor( | ||||||
|  |                 color = HEXAColor(hexaUInt = 0xff0000ffu), | ||||||
|  |                 shortHex = "#f00", | ||||||
|  |                 shortHexa = "#f00f", | ||||||
|  |                 hex = "#ff0000", | ||||||
|  |                 hexa = "#ff0000ff", | ||||||
|  |                 ahex = "#ffff0000", | ||||||
|  |                 ahexUInt = 0xffff0000u, | ||||||
|  |                 rgbUInt = 0xff0000u, | ||||||
|  |                 rgb = "rgb(255,0,0)", | ||||||
|  |                 rgba = "rgba(255,0,0,1.0)", | ||||||
|  |                 r = 0xff, | ||||||
|  |                 g = 0x00, | ||||||
|  |                 b = 0x00, | ||||||
|  |                 a = 0xff, | ||||||
|  |                 "rgba(255,0,0,1)", | ||||||
|  |             ), | ||||||
|  |             TestColor( | ||||||
|  |                 color = HEXAColor(hexaUInt = 0x00ff00ffu), | ||||||
|  |                 shortHex = "#0f0", | ||||||
|  |                 shortHexa = "#0f0f", | ||||||
|  |                 hex = "#00ff00", | ||||||
|  |                 hexa = "#00ff00ff", | ||||||
|  |                 ahex = "#ff00ff00", | ||||||
|  |                 ahexUInt = 0xff00ff00u, | ||||||
|  |                 rgbUInt = 0x00ff00u, | ||||||
|  |                 rgb = "rgb(0,255,0)", | ||||||
|  |                 rgba = "rgba(0,255,0,1.0)", | ||||||
|  |                 r = 0x00, | ||||||
|  |                 g = 0xff, | ||||||
|  |                 b = 0x00, | ||||||
|  |                 a = 0xff, | ||||||
|  |                 "rgba(0,255,0,1)" | ||||||
|  |             ), | ||||||
|  |             TestColor( | ||||||
|  |                 color = HEXAColor(0x0000ffffu), | ||||||
|  |                 shortHex = "#00f", | ||||||
|  |                 shortHexa = "#00ff", | ||||||
|  |                 hex = "#0000ff", | ||||||
|  |                 hexa = "#0000ffff", | ||||||
|  |                 ahex = "#ff0000ff", | ||||||
|  |                 ahexUInt = 0xff0000ffu, | ||||||
|  |                 rgbUInt = 0x0000ffu, | ||||||
|  |                 rgb = "rgb(0,0,255)", | ||||||
|  |                 rgba = "rgba(0,0,255,1.0)", | ||||||
|  |                 r = 0x00, | ||||||
|  |                 g = 0x00, | ||||||
|  |                 b = 0xff, | ||||||
|  |                 a = 0xff, | ||||||
|  |                 "rgba(0,0,255,1)" | ||||||
|  |             ), | ||||||
|  |             TestColor( | ||||||
|  |                 color = HEXAColor(0xff000088u), | ||||||
|  |                 shortHex = "#f00", | ||||||
|  |                 shortHexa = "#f008", | ||||||
|  |                 hex = "#ff0000", | ||||||
|  |                 hexa = "#ff000088", | ||||||
|  |                 ahex = "#88ff0000", | ||||||
|  |                 ahexUInt = 0x88ff0000u, | ||||||
|  |                 rgbUInt = 0xff0000u, | ||||||
|  |                 rgb = "rgb(255,0,0)", | ||||||
|  |                 rgba = "rgba(255,0,0,0.533)", | ||||||
|  |                 r = 0xff, | ||||||
|  |                 g = 0x00, | ||||||
|  |                 b = 0x00, | ||||||
|  |                 a = 0x88, | ||||||
|  |             ), | ||||||
|  |             TestColor( | ||||||
|  |                 color = HEXAColor(0x00ff0088u), | ||||||
|  |                 shortHex = "#0f0", | ||||||
|  |                 shortHexa = "#0f08", | ||||||
|  |                 hex = "#00ff00", | ||||||
|  |                 hexa = "#00ff0088", | ||||||
|  |                 ahex = "#8800ff00", | ||||||
|  |                 ahexUInt = 0x8800ff00u, | ||||||
|  |                 rgbUInt = 0x00ff00u, | ||||||
|  |                 rgb = "rgb(0,255,0)", | ||||||
|  |                 rgba = "rgba(0,255,0,0.533)", | ||||||
|  |                 r = 0x00, | ||||||
|  |                 g = 0xff, | ||||||
|  |                 b = 0x00, | ||||||
|  |                 a = 0x88, | ||||||
|  |             ), | ||||||
|  |             TestColor( | ||||||
|  |                 color = HEXAColor(0x0000ff88u), | ||||||
|  |                 shortHex = "#00f", | ||||||
|  |                 shortHexa = "#00f8", | ||||||
|  |                 hex = "#0000ff", | ||||||
|  |                 hexa = "#0000ff88", | ||||||
|  |                 ahex = "#880000ff", | ||||||
|  |                 ahexUInt = 0x880000ffu, | ||||||
|  |                 rgbUInt = 0x0000ffu, | ||||||
|  |                 rgb = "rgb(0,0,255)", | ||||||
|  |                 rgba = "rgba(0,0,255,0.533)", | ||||||
|  |                 r = 0x00, | ||||||
|  |                 g = 0x00, | ||||||
|  |                 b = 0xff, | ||||||
|  |                 a = 0x88, | ||||||
|  |             ), | ||||||
|  |             TestColor( | ||||||
|  |                 color = HEXAColor(0xff000022u), | ||||||
|  |                 shortHex = "#f00", | ||||||
|  |                 shortHexa = "#f002", | ||||||
|  |                 hex = "#ff0000", | ||||||
|  |                 hexa = "#ff000022", | ||||||
|  |                 ahex = "#22ff0000", | ||||||
|  |                 ahexUInt = 0x22ff0000u, | ||||||
|  |                 rgbUInt = 0xff0000u, | ||||||
|  |                 rgb = "rgb(255,0,0)", | ||||||
|  |                 rgba = "rgba(255,0,0,0.133)", | ||||||
|  |                 r = 0xff, | ||||||
|  |                 g = 0x00, | ||||||
|  |                 b = 0x00, | ||||||
|  |                 a = 0x22, | ||||||
|  |             ), | ||||||
|  |             TestColor( | ||||||
|  |                 color = HEXAColor(0x00ff0022u), | ||||||
|  |                 shortHex = "#0f0", | ||||||
|  |                 shortHexa = "#0f02", | ||||||
|  |                 hex = "#00ff00", | ||||||
|  |                 hexa = "#00ff0022", | ||||||
|  |                 ahex = "#2200ff00", | ||||||
|  |                 ahexUInt = 0x2200ff00u, | ||||||
|  |                 rgbUInt = 0x00ff00u, | ||||||
|  |                 rgb = "rgb(0,255,0)", | ||||||
|  |                 rgba = "rgba(0,255,0,0.133)", | ||||||
|  |                 r = 0x00, | ||||||
|  |                 g = 0xff, | ||||||
|  |                 b = 0x00, | ||||||
|  |                 a = 0x22, | ||||||
|  |             ), | ||||||
|  |             TestColor( | ||||||
|  |                 color = HEXAColor(0x0000ff22u), | ||||||
|  |                 shortHex = "#00f", | ||||||
|  |                 shortHexa = "#00f2", | ||||||
|  |                 hex = "#0000ff", | ||||||
|  |                 hexa = "#0000ff22", | ||||||
|  |                 ahex = "#220000ff", | ||||||
|  |                 ahexUInt = 0x220000ffu, | ||||||
|  |                 rgbUInt = 0x0000ffu, | ||||||
|  |                 rgb = "rgb(0,0,255)", | ||||||
|  |                 rgba = "rgba(0,0,255,0.133)", | ||||||
|  |                 r = 0x00, | ||||||
|  |                 g = 0x00, | ||||||
|  |                 b = 0xff, | ||||||
|  |                 a = 0x22, | ||||||
|  |             ), | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |     @Test | ||||||
|  |     fun baseTest() { | ||||||
|  |         testColors.forEach { | ||||||
|  |             assertEquals(it.hex, it.color.hex) | ||||||
|  |             assertEquals(it.hexa, it.color.hexa) | ||||||
|  |             assertEquals(it.ahex, it.color.ahex) | ||||||
|  |             assertEquals(it.rgbUInt, it.color.rgbUInt) | ||||||
|  |             assertEquals(it.ahexUInt, it.color.ahexUInt) | ||||||
|  |             assertEquals(it.shortHex, it.color.shortHex) | ||||||
|  |             assertEquals(it.shortHexa, it.color.shortHexa) | ||||||
|  |             assertEquals(it.rgb, it.color.rgb) | ||||||
|  |             assertTrue(it.rgba == it.color.rgba || it.color.rgba in it.additionalRGBAVariants) | ||||||
|  |             assertEquals(it.r, it.color.r) | ||||||
|  |             assertEquals(it.g, it.color.g) | ||||||
|  |             assertEquals(it.b, it.color.b) | ||||||
|  |             assertEquals(it.a, it.color.a) | ||||||
|  |             assertEquals(it.color, HEXAColor.fromAhex(it.ahexUInt)) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Test | ||||||
|  |     fun testHexParseColor() { | ||||||
|  |         testColors.forEach { | ||||||
|  |             assertEquals(it.color.copy(aOfOne = 1f), HEXAColor.parseStringColor(it.hex)) | ||||||
|  |             assertEquals(it.color, HEXAColor.parseStringColor(it.hexa)) | ||||||
|  |             assertEquals(it.color.copy(aOfOne = 1f), HEXAColor.parseStringColor(it.rgb)) | ||||||
|  |             assertTrue(it.color.hexaUInt.toInt() - HEXAColor.parseStringColor(it.rgba).hexaUInt.toInt() in -0x1 .. 0x1, ) | ||||||
|  |             assertEquals(it.color.copy(aOfOne = 1f), HEXAColor.parseStringColor(it.shortHex)) | ||||||
|  |             assertEquals(it.color.copy(a = floor(it.color.a.toFloat() / 16).toInt() * 0x10), HEXAColor.parseStringColor(it.shortHexa)) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										298
									
								
								colors/src/commonMain/kotlin/StandardColors.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										298
									
								
								colors/src/commonMain/kotlin/StandardColors.kt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,298 @@ | |||||||
|  | package dev.inmo.micro_utils.colors | ||||||
|  |  | ||||||
|  | import dev.inmo.micro_utils.colors.common.HEXAColor | ||||||
|  |  | ||||||
|  | val HEXAColor.Companion.aliceblue | ||||||
|  |     get() = HEXAColor(0xF0F8FFFFu) | ||||||
|  | val HEXAColor.Companion.antiquewhite | ||||||
|  |     get() = HEXAColor(0xFAEBD7FFu) | ||||||
|  | val HEXAColor.Companion.aqua | ||||||
|  |     get() = HEXAColor(0x00FFFFFFu) | ||||||
|  | val HEXAColor.Companion.aquamarine | ||||||
|  |     get() = HEXAColor(0x7FFFD4FFu) | ||||||
|  | val HEXAColor.Companion.azure | ||||||
|  |     get() = HEXAColor(0xF0FFFFFFu) | ||||||
|  | val HEXAColor.Companion.beige | ||||||
|  |     get() = HEXAColor(0xF5F5DCFFu) | ||||||
|  | val HEXAColor.Companion.bisque | ||||||
|  |     get() = HEXAColor(0xFFE4C4FFu) | ||||||
|  | val HEXAColor.Companion.black | ||||||
|  |     get() = HEXAColor(0x000000FFu) | ||||||
|  | val HEXAColor.Companion.blanchedalmond | ||||||
|  |     get() = HEXAColor(0xFFEBCDFFu) | ||||||
|  | val HEXAColor.Companion.blue | ||||||
|  |     get() = HEXAColor(0x0000FFFFu) | ||||||
|  | val HEXAColor.Companion.blueviolet | ||||||
|  |     get() = HEXAColor(0x8A2BE2FFu) | ||||||
|  | val HEXAColor.Companion.brown | ||||||
|  |     get() = HEXAColor(0xA52A2AFFu) | ||||||
|  | val HEXAColor.Companion.burlywood | ||||||
|  |     get() = HEXAColor(0xDEB887FFu) | ||||||
|  | val HEXAColor.Companion.cadetblue | ||||||
|  |     get() = HEXAColor(0x5F9EA0FFu) | ||||||
|  | val HEXAColor.Companion.chartreuse | ||||||
|  |     get() = HEXAColor(0x7FFF00FFu) | ||||||
|  | val HEXAColor.Companion.chocolate | ||||||
|  |     get() = HEXAColor(0xD2691EFFu) | ||||||
|  | val HEXAColor.Companion.coral | ||||||
|  |     get() = HEXAColor(0xFF7F50FFu) | ||||||
|  | val HEXAColor.Companion.cornflowerblue | ||||||
|  |     get() = HEXAColor(0x6495EDFFu) | ||||||
|  | val HEXAColor.Companion.cornsilk | ||||||
|  |     get() = HEXAColor(0xFFF8DCFFu) | ||||||
|  | val HEXAColor.Companion.crimson | ||||||
|  |     get() = HEXAColor(0xDC143CFFu) | ||||||
|  | val HEXAColor.Companion.cyan | ||||||
|  |     get() = HEXAColor(0x00FFFFFFu) | ||||||
|  | val HEXAColor.Companion.darkblue | ||||||
|  |     get() = HEXAColor(0x00008BFFu) | ||||||
|  | val HEXAColor.Companion.darkcyan | ||||||
|  |     get() = HEXAColor(0x008B8BFFu) | ||||||
|  | val HEXAColor.Companion.darkgoldenrod | ||||||
|  |     get() = HEXAColor(0xB8860BFFu) | ||||||
|  | val HEXAColor.Companion.darkgray | ||||||
|  |     get() = HEXAColor(0xA9A9A9FFu) | ||||||
|  | val HEXAColor.Companion.darkgreen | ||||||
|  |     get() = HEXAColor(0x006400FFu) | ||||||
|  | val HEXAColor.Companion.darkgrey | ||||||
|  |     get() = HEXAColor(0xA9A9A9FFu) | ||||||
|  | val HEXAColor.Companion.darkkhaki | ||||||
|  |     get() = HEXAColor(0xBDB76BFFu) | ||||||
|  | val HEXAColor.Companion.darkmagenta | ||||||
|  |     get() = HEXAColor(0x8B008BFFu) | ||||||
|  | val HEXAColor.Companion.darkolivegreen | ||||||
|  |     get() = HEXAColor(0x556B2FFFu) | ||||||
|  | val HEXAColor.Companion.darkorange | ||||||
|  |     get() = HEXAColor(0xFF8C00FFu) | ||||||
|  | val HEXAColor.Companion.darkorchid | ||||||
|  |     get() = HEXAColor(0x9932CCFFu) | ||||||
|  | val HEXAColor.Companion.darkred | ||||||
|  |     get() = HEXAColor(0x8B0000FFu) | ||||||
|  | val HEXAColor.Companion.darksalmon | ||||||
|  |     get() = HEXAColor(0xE9967AFFu) | ||||||
|  | val HEXAColor.Companion.darkseagreen | ||||||
|  |     get() = HEXAColor(0x8FBC8FFFu) | ||||||
|  | val HEXAColor.Companion.darkslateblue | ||||||
|  |     get() = HEXAColor(0x483D8BFFu) | ||||||
|  | val HEXAColor.Companion.darkslategray | ||||||
|  |     get() = HEXAColor(0x2F4F4FFFu) | ||||||
|  | val HEXAColor.Companion.darkslategrey | ||||||
|  |     get() = HEXAColor(0x2F4F4FFFu) | ||||||
|  | val HEXAColor.Companion.darkturquoise | ||||||
|  |     get() = HEXAColor(0x00CED1FFu) | ||||||
|  | val HEXAColor.Companion.darkviolet | ||||||
|  |     get() = HEXAColor(0x9400D3FFu) | ||||||
|  | val HEXAColor.Companion.deeppink | ||||||
|  |     get() = HEXAColor(0xFF1493FFu) | ||||||
|  | val HEXAColor.Companion.deepskyblue | ||||||
|  |     get() = HEXAColor(0x00BFFFFFu) | ||||||
|  | val HEXAColor.Companion.dimgray | ||||||
|  |     get() = HEXAColor(0x696969FFu) | ||||||
|  | val HEXAColor.Companion.dimgrey | ||||||
|  |     get() = HEXAColor(0x696969FFu) | ||||||
|  | val HEXAColor.Companion.dodgerblue | ||||||
|  |     get() = HEXAColor(0x1E90FFFFu) | ||||||
|  | val HEXAColor.Companion.firebrick | ||||||
|  |     get() = HEXAColor(0xB22222FFu) | ||||||
|  | val HEXAColor.Companion.floralwhite | ||||||
|  |     get() = HEXAColor(0xFFFAF0FFu) | ||||||
|  | val HEXAColor.Companion.forestgreen | ||||||
|  |     get() = HEXAColor(0x228B22FFu) | ||||||
|  | val HEXAColor.Companion.fuchsia | ||||||
|  |     get() = HEXAColor(0xFF00FFFFu) | ||||||
|  | val HEXAColor.Companion.gainsboro | ||||||
|  |     get() = HEXAColor(0xDCDCDCFFu) | ||||||
|  | val HEXAColor.Companion.ghostwhite | ||||||
|  |     get() = HEXAColor(0xF8F8FFFFu) | ||||||
|  | val HEXAColor.Companion.gold | ||||||
|  |     get() = HEXAColor(0xFFD700FFu) | ||||||
|  | val HEXAColor.Companion.goldenrod | ||||||
|  |     get() = HEXAColor(0xDAA520FFu) | ||||||
|  | val HEXAColor.Companion.gray | ||||||
|  |     get() = HEXAColor(0x808080FFu) | ||||||
|  | val HEXAColor.Companion.green | ||||||
|  |     get() = HEXAColor(0x008000FFu) | ||||||
|  | val HEXAColor.Companion.greenyellow | ||||||
|  |     get() = HEXAColor(0xADFF2FFFu) | ||||||
|  | val HEXAColor.Companion.grey | ||||||
|  |     get() = HEXAColor(0x808080FFu) | ||||||
|  | val HEXAColor.Companion.honeydew | ||||||
|  |     get() = HEXAColor(0xF0FFF0FFu) | ||||||
|  | val HEXAColor.Companion.hotpink | ||||||
|  |     get() = HEXAColor(0xFF69B4FFu) | ||||||
|  | val HEXAColor.Companion.indianred | ||||||
|  |     get() = HEXAColor(0xCD5C5CFFu) | ||||||
|  | val HEXAColor.Companion.indigo | ||||||
|  |     get() = HEXAColor(0x4B0082FFu) | ||||||
|  | val HEXAColor.Companion.ivory | ||||||
|  |     get() = HEXAColor(0xFFFFF0FFu) | ||||||
|  | val HEXAColor.Companion.khaki | ||||||
|  |     get() = HEXAColor(0xF0E68CFFu) | ||||||
|  | val HEXAColor.Companion.lavender | ||||||
|  |     get() = HEXAColor(0xE6E6FAFFu) | ||||||
|  | val HEXAColor.Companion.lavenderblush | ||||||
|  |     get() = HEXAColor(0xFFF0F5FFu) | ||||||
|  | val HEXAColor.Companion.lawngreen | ||||||
|  |     get() = HEXAColor(0x7CFC00FFu) | ||||||
|  | val HEXAColor.Companion.lemonchiffon | ||||||
|  |     get() = HEXAColor(0xFFFACDFFu) | ||||||
|  | val HEXAColor.Companion.lightblue | ||||||
|  |     get() = HEXAColor(0xADD8E6FFu) | ||||||
|  | val HEXAColor.Companion.lightcoral | ||||||
|  |     get() = HEXAColor(0xF08080FFu) | ||||||
|  | val HEXAColor.Companion.lightcyan | ||||||
|  |     get() = HEXAColor(0xE0FFFFFFu) | ||||||
|  | val HEXAColor.Companion.lightgoldenrodyellow | ||||||
|  |     get() = HEXAColor(0xFAFAD2FFu) | ||||||
|  | val HEXAColor.Companion.lightgray | ||||||
|  |     get() = HEXAColor(0xD3D3D3FFu) | ||||||
|  | val HEXAColor.Companion.lightgreen | ||||||
|  |     get() = HEXAColor(0x90EE90FFu) | ||||||
|  | val HEXAColor.Companion.lightgrey | ||||||
|  |     get() = HEXAColor(0xD3D3D3FFu) | ||||||
|  | val HEXAColor.Companion.lightpink | ||||||
|  |     get() = HEXAColor(0xFFB6C1FFu) | ||||||
|  | val HEXAColor.Companion.lightsalmon | ||||||
|  |     get() = HEXAColor(0xFFA07AFFu) | ||||||
|  | val HEXAColor.Companion.lightseagreen | ||||||
|  |     get() = HEXAColor(0x20B2AAFFu) | ||||||
|  | val HEXAColor.Companion.lightskyblue | ||||||
|  |     get() = HEXAColor(0x87CEFAFFu) | ||||||
|  | val HEXAColor.Companion.lightslategray | ||||||
|  |     get() = HEXAColor(0x778899FFu) | ||||||
|  | val HEXAColor.Companion.lightslategrey | ||||||
|  |     get() = HEXAColor(0x778899FFu) | ||||||
|  | val HEXAColor.Companion.lightsteelblue | ||||||
|  |     get() = HEXAColor(0xB0C4DEFFu) | ||||||
|  | val HEXAColor.Companion.lightyellow | ||||||
|  |     get() = HEXAColor(0xFFFFE0FFu) | ||||||
|  | val HEXAColor.Companion.lime | ||||||
|  |     get() = HEXAColor(0x00FF00FFu) | ||||||
|  | val HEXAColor.Companion.limegreen | ||||||
|  |     get() = HEXAColor(0x32CD32FFu) | ||||||
|  | val HEXAColor.Companion.linen | ||||||
|  |     get() = HEXAColor(0xFAF0E6FFu) | ||||||
|  | val HEXAColor.Companion.magenta | ||||||
|  |     get() = HEXAColor(0xFF00FFFFu) | ||||||
|  | val HEXAColor.Companion.maroon | ||||||
|  |     get() = HEXAColor(0x800000FFu) | ||||||
|  | val HEXAColor.Companion.mediumaquamarine | ||||||
|  |     get() = HEXAColor(0x66CDAAFFu) | ||||||
|  | val HEXAColor.Companion.mediumblue | ||||||
|  |     get() = HEXAColor(0x0000CDFFu) | ||||||
|  | val HEXAColor.Companion.mediumorchid | ||||||
|  |     get() = HEXAColor(0xBA55D3FFu) | ||||||
|  | val HEXAColor.Companion.mediumpurple | ||||||
|  |     get() = HEXAColor(0x9370DBFFu) | ||||||
|  | val HEXAColor.Companion.mediumseagreen | ||||||
|  |     get() = HEXAColor(0x3CB371FFu) | ||||||
|  | val HEXAColor.Companion.mediumslateblue | ||||||
|  |     get() = HEXAColor(0x7B68EEFFu) | ||||||
|  | val HEXAColor.Companion.mediumspringgreen | ||||||
|  |     get() = HEXAColor(0x00FA9AFFu) | ||||||
|  | val HEXAColor.Companion.mediumturquoise | ||||||
|  |     get() = HEXAColor(0x48D1CCFFu) | ||||||
|  | val HEXAColor.Companion.mediumvioletred | ||||||
|  |     get() = HEXAColor(0xC71585FFu) | ||||||
|  | val HEXAColor.Companion.midnightblue | ||||||
|  |     get() = HEXAColor(0x191970FFu) | ||||||
|  | val HEXAColor.Companion.mintcream | ||||||
|  |     get() = HEXAColor(0xF5FFFAFFu) | ||||||
|  | val HEXAColor.Companion.mistyrose | ||||||
|  |     get() = HEXAColor(0xFFE4E1FFu) | ||||||
|  | val HEXAColor.Companion.moccasin | ||||||
|  |     get() = HEXAColor(0xFFE4B5FFu) | ||||||
|  | val HEXAColor.Companion.navajowhite | ||||||
|  |     get() = HEXAColor(0xFFDEADFFu) | ||||||
|  | val HEXAColor.Companion.navy | ||||||
|  |     get() = HEXAColor(0x000080FFu) | ||||||
|  | val HEXAColor.Companion.oldlace | ||||||
|  |     get() = HEXAColor(0xFDF5E6FFu) | ||||||
|  | val HEXAColor.Companion.olive | ||||||
|  |     get() = HEXAColor(0x808000FFu) | ||||||
|  | val HEXAColor.Companion.olivedrab | ||||||
|  |     get() = HEXAColor(0x6B8E23FFu) | ||||||
|  | val HEXAColor.Companion.orange | ||||||
|  |     get() = HEXAColor(0xFFA500FFu) | ||||||
|  | val HEXAColor.Companion.orangered | ||||||
|  |     get() = HEXAColor(0xFF4500FFu) | ||||||
|  | val HEXAColor.Companion.orchid | ||||||
|  |     get() = HEXAColor(0xDA70D6FFu) | ||||||
|  | val HEXAColor.Companion.palegoldenrod | ||||||
|  |     get() = HEXAColor(0xEEE8AAFFu) | ||||||
|  | val HEXAColor.Companion.palegreen | ||||||
|  |     get() = HEXAColor(0x98FB98FFu) | ||||||
|  | val HEXAColor.Companion.paleturquoise | ||||||
|  |     get() = HEXAColor(0xAFEEEEFFu) | ||||||
|  | val HEXAColor.Companion.palevioletred | ||||||
|  |     get() = HEXAColor(0xDB7093FFu) | ||||||
|  | val HEXAColor.Companion.papayawhip | ||||||
|  |     get() = HEXAColor(0xFFEFD5FFu) | ||||||
|  | val HEXAColor.Companion.peachpuff | ||||||
|  |     get() = HEXAColor(0xFFDAB9FFu) | ||||||
|  | val HEXAColor.Companion.peru | ||||||
|  |     get() = HEXAColor(0xCD853FFFu) | ||||||
|  | val HEXAColor.Companion.pink | ||||||
|  |     get() = HEXAColor(0xFFC0CBFFu) | ||||||
|  | val HEXAColor.Companion.plum | ||||||
|  |     get() = HEXAColor(0xDDA0DDFFu) | ||||||
|  | val HEXAColor.Companion.powderblue | ||||||
|  |     get() = HEXAColor(0xB0E0E6FFu) | ||||||
|  | val HEXAColor.Companion.purple | ||||||
|  |     get() = HEXAColor(0x800080FFu) | ||||||
|  | val HEXAColor.Companion.red | ||||||
|  |     get() = HEXAColor(0xFF0000FFu) | ||||||
|  | val HEXAColor.Companion.rosybrown | ||||||
|  |     get() = HEXAColor(0xBC8F8FFFu) | ||||||
|  | val HEXAColor.Companion.royalblue | ||||||
|  |     get() = HEXAColor(0x4169E1FFu) | ||||||
|  | val HEXAColor.Companion.saddlebrown | ||||||
|  |     get() = HEXAColor(0x8B4513FFu) | ||||||
|  | val HEXAColor.Companion.salmon | ||||||
|  |     get() = HEXAColor(0xFA8072FFu) | ||||||
|  | val HEXAColor.Companion.sandybrown | ||||||
|  |     get() = HEXAColor(0xF4A460FFu) | ||||||
|  | val HEXAColor.Companion.seagreen | ||||||
|  |     get() = HEXAColor(0x2E8B57FFu) | ||||||
|  | val HEXAColor.Companion.seashell | ||||||
|  |     get() = HEXAColor(0xFFF5EEFFu) | ||||||
|  | val HEXAColor.Companion.sienna | ||||||
|  |     get() = HEXAColor(0xA0522DFFu) | ||||||
|  | val HEXAColor.Companion.silver | ||||||
|  |     get() = HEXAColor(0xC0C0C0FFu) | ||||||
|  | val HEXAColor.Companion.skyblue | ||||||
|  |     get() = HEXAColor(0x87CEEBFFu) | ||||||
|  | val HEXAColor.Companion.slateblue | ||||||
|  |     get() = HEXAColor(0x6A5ACDFFu) | ||||||
|  | val HEXAColor.Companion.slategray | ||||||
|  |     get() = HEXAColor(0x708090FFu) | ||||||
|  | val HEXAColor.Companion.slategrey | ||||||
|  |     get() = HEXAColor(0x708090FFu) | ||||||
|  | val HEXAColor.Companion.snow | ||||||
|  |     get() = HEXAColor(0xFFFAFAFFu) | ||||||
|  | val HEXAColor.Companion.springgreen | ||||||
|  |     get() = HEXAColor(0x00FF7FFFu) | ||||||
|  | val HEXAColor.Companion.steelblue | ||||||
|  |     get() = HEXAColor(0x4682B4FFu) | ||||||
|  | val HEXAColor.Companion.tan | ||||||
|  |     get() = HEXAColor(0xD2B48CFFu) | ||||||
|  | val HEXAColor.Companion.teal | ||||||
|  |     get() = HEXAColor(0x008080FFu) | ||||||
|  | val HEXAColor.Companion.thistle | ||||||
|  |     get() = HEXAColor(0xD8BFD8FFu) | ||||||
|  | val HEXAColor.Companion.tomato | ||||||
|  |     get() = HEXAColor(0xFF6347FFu) | ||||||
|  | val HEXAColor.Companion.turquoise | ||||||
|  |     get() = HEXAColor(0x40E0D0FFu) | ||||||
|  | val HEXAColor.Companion.violet | ||||||
|  |     get() = HEXAColor(0xEE82EEFFu) | ||||||
|  | val HEXAColor.Companion.wheat | ||||||
|  |     get() = HEXAColor(0xF5DEB3FFu) | ||||||
|  | val HEXAColor.Companion.white | ||||||
|  |     get() = HEXAColor(0xFFFFFFFFu) | ||||||
|  | val HEXAColor.Companion.whitesmoke | ||||||
|  |     get() = HEXAColor(0xF5F5F5FFu) | ||||||
|  | val HEXAColor.Companion.yellow | ||||||
|  |     get() = HEXAColor(0xFFFF00FFu) | ||||||
|  | val HEXAColor.Companion.yellowgreen | ||||||
|  |     get() = HEXAColor(0x9ACD32FFu) | ||||||
| @@ -1,6 +1,40 @@ | |||||||
| plugins { | plugins { | ||||||
|     id "org.jetbrains.kotlin.multiplatform" |     id "org.jetbrains.kotlin.multiplatform" | ||||||
|     id "org.jetbrains.kotlin.plugin.serialization" |     id "org.jetbrains.kotlin.plugin.serialization" | ||||||
|  |     id "com.android.library" | ||||||
| } | } | ||||||
|  |  | ||||||
| apply from: "$mppProjectWithSerializationPresetPath" | apply from: "$mppJvmJsWasmJsAndroidLinuxMingwLinuxArm64Project" | ||||||
|  |  | ||||||
|  | kotlin { | ||||||
|  |     sourceSets { | ||||||
|  |         commonMain { | ||||||
|  |             dependencies { | ||||||
|  |                 api libs.klock | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         jvmMain { | ||||||
|  |             dependencies { | ||||||
|  |                 api project(":micro_utils.coroutines") | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         androidMain { | ||||||
|  |             dependencies { | ||||||
|  |                 api libs.android.fragment | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         nativeMain { | ||||||
|  |             dependencies { | ||||||
|  |                 api libs.okio | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         wasmJsMain { | ||||||
|  |             dependencies { | ||||||
|  |                 api libs.kotlinx.browser | ||||||
|  |                 api libs.kt.coroutines | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|   | |||||||
							
								
								
									
										20
									
								
								common/compose/build.gradle
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								common/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) | ||||||
|  |     alias(libs.plugins.kt.jb.compose) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | apply from: "$mppComposeJvmJsWasmJsAndroidLinuxMingwLinuxArm64Project" | ||||||
|  |  | ||||||
|  | kotlin { | ||||||
|  |     sourceSets { | ||||||
|  |         commonMain { | ||||||
|  |             dependencies { | ||||||
|  |                 api project(":micro_utils.common") | ||||||
|  |                 api libs.kt.coroutines | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										1
									
								
								common/compose/src/androidMain/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								common/compose/src/androidMain/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | <manifest/> | ||||||
| @@ -0,0 +1,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,74 @@ | |||||||
|  | package dev.inmo.micro_utils.common.compose | ||||||
|  |  | ||||||
|  | import androidx.compose.runtime.* | ||||||
|  | import dev.inmo.micro_utils.common.Optional | ||||||
|  | import dev.inmo.micro_utils.common.dataOrThrow | ||||||
|  | import dev.inmo.micro_utils.common.optional | ||||||
|  |  | ||||||
|  | class LoadableComponentContext<T> internal constructor( | ||||||
|  |     presetOptional: Optional<T>, | ||||||
|  | ) { | ||||||
|  |     internal val iterationState: MutableState<Int> = mutableStateOf(0) | ||||||
|  |  | ||||||
|  |     internal var dataOptional: Optional<T> = if (presetOptional.dataPresented) presetOptional else Optional.absent() | ||||||
|  |         private set | ||||||
|  |     internal val dataState: MutableState<Optional<T>> = mutableStateOf(dataOptional) | ||||||
|  |  | ||||||
|  |     fun reload() { | ||||||
|  |         iterationState.value++ | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Showing data with ability to reload data | ||||||
|  |  * | ||||||
|  |  * [block] will be shown when [loader] will complete loading. If you want to reload data, just call | ||||||
|  |  * [LoadableComponentContext.reload] | ||||||
|  |  */ | ||||||
|  | @Composable | ||||||
|  | fun <T> LoadableComponent( | ||||||
|  |     preload: Optional<T>, | ||||||
|  |     loader: suspend LoadableComponentContext<T>.() -> T, | ||||||
|  |     block: @Composable LoadableComponentContext<T>.(T) -> Unit | ||||||
|  | ) { | ||||||
|  |     val context = remember { LoadableComponentContext(preload) } | ||||||
|  |  | ||||||
|  |     LaunchedEffect(context.iterationState.value) { | ||||||
|  |         context.dataState.value = loader(context).optional | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     context.dataState.let { | ||||||
|  |         if (it.value.dataPresented) { | ||||||
|  |             context.block(it.value.dataOrThrow(IllegalStateException("Data must be presented, but optional has been changed by some way"))) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Showing data with ability to reload data | ||||||
|  |  * | ||||||
|  |  * [block] will be shown when [loader] will complete loading. If you want to reload data, just call | ||||||
|  |  * [LoadableComponentContext.reload] | ||||||
|  |  */ | ||||||
|  | @Composable | ||||||
|  | fun <T> LoadableComponent( | ||||||
|  |     preload: T, | ||||||
|  |     loader: suspend LoadableComponentContext<T>.() -> T, | ||||||
|  |     block: @Composable LoadableComponentContext<T>.(T) -> Unit | ||||||
|  | ) { | ||||||
|  |     LoadableComponent(preload.optional, loader, block) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Showing data with ability to reload data | ||||||
|  |  * | ||||||
|  |  * [block] will be shown when [loader] will complete loading. If you want to reload data, just call | ||||||
|  |  * [LoadableComponentContext.reload] | ||||||
|  |  */ | ||||||
|  | @Composable | ||||||
|  | fun <T> LoadableComponent( | ||||||
|  |     loader: suspend LoadableComponentContext<T>.() -> T, | ||||||
|  |     block: @Composable LoadableComponentContext<T>.(T) -> Unit | ||||||
|  | ) { | ||||||
|  |     LoadableComponent(Optional.absent(), loader, block) | ||||||
|  | } | ||||||
| @@ -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,19 @@ | |||||||
|  | package dev.inmo.micro_utils.common.compose | ||||||
|  |  | ||||||
|  | import org.jetbrains.compose.web.dom.AttrBuilderContext | ||||||
|  | import org.w3c.dom.Element | ||||||
|  |  | ||||||
|  | operator fun <T : Element> AttrBuilderContext<T>?.plus( | ||||||
|  |     other: AttrBuilderContext<T>? | ||||||
|  | ) = when (this) { | ||||||
|  |     null -> other ?: {} | ||||||
|  |     else -> when (other) { | ||||||
|  |         null -> this ?: {} | ||||||
|  |         else -> { | ||||||
|  |             { | ||||||
|  |                 invoke(this) | ||||||
|  |                 other(this) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,22 @@ | |||||||
|  | package dev.inmo.micro_utils.common.compose | ||||||
|  |  | ||||||
|  | import androidx.compose.runtime.Composable | ||||||
|  | import androidx.compose.runtime.DisposableEffect | ||||||
|  | import androidx.compose.runtime.DisposableEffectResult | ||||||
|  | import androidx.compose.runtime.DisposableEffectScope | ||||||
|  | import org.jetbrains.compose.web.attributes.AttrsScope | ||||||
|  | import org.jetbrains.compose.web.dom.ElementScope | ||||||
|  | import org.w3c.dom.Element | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * This function must be called in the context of your tag content. It works like default [AttrsScope.ref], | ||||||
|  |  * but able to be used several times. Uses [DisposableEffect] under the hood | ||||||
|  |  */ | ||||||
|  | @Composable | ||||||
|  | fun <T : Element> ElementScope<T>.ref( | ||||||
|  |     block: DisposableEffectScope.(T) -> DisposableEffectResult | ||||||
|  | ) { | ||||||
|  |     DisposableEffect(0) { | ||||||
|  |         block(scopeElement) | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,10 @@ | |||||||
|  | package dev.inmo.micro_utils.common.compose | ||||||
|  |  | ||||||
|  | import org.jetbrains.compose.web.attributes.ATarget | ||||||
|  |  | ||||||
|  | fun openLink(link: String, mode: ATarget = ATarget.Blank, features: String = "") = dev.inmo.micro_utils.common.openLink( | ||||||
|  |     link, | ||||||
|  |     mode.targetStr, | ||||||
|  |     features | ||||||
|  | ) | ||||||
|  |  | ||||||
| @@ -0,0 +1,13 @@ | |||||||
|  | package dev.inmo.micro_utils.common.compose | ||||||
|  |  | ||||||
|  | import androidx.compose.runtime.* | ||||||
|  | import org.jetbrains.compose.web.dom.DOMScope | ||||||
|  | import org.w3c.dom.Element | ||||||
|  |  | ||||||
|  | fun <TElement : Element> renderComposableAndLinkToRoot( | ||||||
|  |     root: TElement, | ||||||
|  |     monotonicFrameClock: MonotonicFrameClock = DefaultMonotonicFrameClock, | ||||||
|  |     content: @Composable DOMScope<TElement>.() -> Unit | ||||||
|  | ): Composition = org.jetbrains.compose.web.renderComposable(root, monotonicFrameClock, content).apply { | ||||||
|  |     linkWithElement(root) | ||||||
|  | } | ||||||
| @@ -0,0 +1,43 @@ | |||||||
|  | package dev.inmo.micro_utils.common.compose | ||||||
|  |  | ||||||
|  | import org.jetbrains.compose.web.css.* | ||||||
|  |  | ||||||
|  | object SkeletonAnimation : StyleSheet() { | ||||||
|  |     val skeletonKeyFrames: CSSNamedKeyframes by keyframes { | ||||||
|  |         to { backgroundPosition("-20% 0") } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fun CSSBuilder.includeSkeletonStyle( | ||||||
|  |         duration: CSSSizeValue<out CSSUnitTime> = 2.s, | ||||||
|  |         timingFunction: AnimationTimingFunction = AnimationTimingFunction.EaseInOut, | ||||||
|  |         iterationCount: Int? = null, | ||||||
|  |         direction: AnimationDirection = AnimationDirection.Normal, | ||||||
|  |         keyFrames: CSSNamedKeyframes = skeletonKeyFrames, | ||||||
|  |         hideChildren: Boolean = true, | ||||||
|  |         hideText: Boolean = hideChildren | ||||||
|  |     ) { | ||||||
|  |         backgroundImage("linear-gradient(110deg, rgb(236, 236, 236) 40%, rgb(245, 245, 245) 50%, rgb(236, 236, 236) 65%)") | ||||||
|  |         backgroundSize("200% 100%") | ||||||
|  |         backgroundPosition("180% 0") | ||||||
|  |         animation(keyFrames) { | ||||||
|  |             duration(duration) | ||||||
|  |             timingFunction(timingFunction) | ||||||
|  |             iterationCount(iterationCount) | ||||||
|  |             direction(direction) | ||||||
|  |         } | ||||||
|  |         if (hideText) { | ||||||
|  |             property("color", "${Color.transparent} !important") | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (hideChildren) { | ||||||
|  |             child(self, universal) style { | ||||||
|  |                 property("visibility", "hidden") | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     val skeleton by style { | ||||||
|  |         includeSkeletonStyle() | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
| @@ -0,0 +1,11 @@ | |||||||
|  | package dev.inmo.micro_utils.common.compose | ||||||
|  |  | ||||||
|  | import org.jetbrains.compose.web.dom.AttrBuilderContext | ||||||
|  |  | ||||||
|  | fun tagClasses(vararg classnames: String): AttrBuilderContext<*> = { | ||||||
|  |     classes(*classnames) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fun tagId(id: String): AttrBuilderContext<*> = { | ||||||
|  |     id(id) | ||||||
|  | } | ||||||
							
								
								
									
										40
									
								
								common/compose/src/jvmTest/kotlin/LoadableComponentTests.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								common/compose/src/jvmTest/kotlin/LoadableComponentTests.kt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,40 @@ | |||||||
|  | import androidx.compose.runtime.remember | ||||||
|  | import androidx.compose.ui.test.ExperimentalTestApi | ||||||
|  | import androidx.compose.ui.test.runComposeUiTest | ||||||
|  | import dev.inmo.micro_utils.common.compose.LoadableComponent | ||||||
|  | import dev.inmo.micro_utils.coroutines.MutableRedeliverStateFlow | ||||||
|  | import kotlinx.coroutines.flow.filter | ||||||
|  | import kotlinx.coroutines.flow.first | ||||||
|  | import org.jetbrains.annotations.TestOnly | ||||||
|  | import kotlin.test.Test | ||||||
|  | import kotlin.test.assertTrue | ||||||
|  |  | ||||||
|  | class LoadableComponentTests { | ||||||
|  |     @OptIn(ExperimentalTestApi::class) | ||||||
|  |     @Test | ||||||
|  |     @TestOnly | ||||||
|  |     fun testSimpleLoad() = runComposeUiTest { | ||||||
|  |         val loadingFlow = MutableRedeliverStateFlow<Int>(0) | ||||||
|  |         val loadedFlow = MutableRedeliverStateFlow<Int>(0) | ||||||
|  |         setContent { | ||||||
|  |             LoadableComponent<Int>({ | ||||||
|  |                 loadingFlow.filter { it == 1 }.first() | ||||||
|  |             }) { | ||||||
|  |                 assert(dataState.value.data == 1) | ||||||
|  |                 remember { | ||||||
|  |                     loadedFlow.value = 2 | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         waitForIdle() | ||||||
|  |  | ||||||
|  |         assertTrue(loadedFlow.value == 0) | ||||||
|  |  | ||||||
|  |         loadingFlow.value = 1 | ||||||
|  |  | ||||||
|  |         waitForIdle() | ||||||
|  |  | ||||||
|  |         assertTrue(loadedFlow.value == 2) | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,76 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | import android.os.Bundle | ||||||
|  | import android.os.Parcelable | ||||||
|  | import androidx.fragment.app.Fragment | ||||||
|  | import java.io.Serializable | ||||||
|  | import kotlin.reflect.KProperty | ||||||
|  |  | ||||||
|  | object ArgumentPropertyNullableDelegate { | ||||||
|  |     operator fun <T: Any> getValue(thisRef: Fragment, property: KProperty<*>): T? { | ||||||
|  |         val arguments = thisRef.arguments ?: return null | ||||||
|  |         val key = property.name | ||||||
|  |         return when (property.getter.returnType.classifier) { | ||||||
|  |             // Scalars | ||||||
|  |             String::class -> arguments.getString(key) | ||||||
|  |             Boolean::class -> arguments.getBoolean(key) | ||||||
|  |             Byte::class -> arguments.getByte(key) | ||||||
|  |             Char::class -> arguments.getChar(key) | ||||||
|  |             Double::class -> arguments.getDouble(key) | ||||||
|  |             Float::class -> arguments.getFloat(key) | ||||||
|  |             Int::class -> arguments.getInt(key) | ||||||
|  |             Long::class -> arguments.getLong(key) | ||||||
|  |             Short::class -> arguments.getShort(key) | ||||||
|  |  | ||||||
|  |             // References | ||||||
|  |             Bundle::class -> arguments.getBundle(key) | ||||||
|  |             CharSequence::class -> arguments.getCharSequence(key) | ||||||
|  |             Parcelable::class -> arguments.getParcelable(key) | ||||||
|  |  | ||||||
|  |             // Scalar arrays | ||||||
|  |             BooleanArray::class -> arguments.getBooleanArray(key) | ||||||
|  |             ByteArray::class -> arguments.getByteArray(key) | ||||||
|  |             CharArray::class -> arguments.getCharArray(key) | ||||||
|  |             DoubleArray::class -> arguments.getDoubleArray(key) | ||||||
|  |             FloatArray::class -> arguments.getFloatArray(key) | ||||||
|  |             IntArray::class -> arguments.getIntArray(key) | ||||||
|  |             LongArray::class -> arguments.getLongArray(key) | ||||||
|  |             ShortArray::class -> arguments.getShortArray(key) | ||||||
|  |             Array::class -> { | ||||||
|  |                 val componentType = property.returnType.classifier ?.javaClass ?.componentType!! | ||||||
|  |                 @Suppress("UNCHECKED_CAST") // Checked by reflection. | ||||||
|  |                 when { | ||||||
|  |                     Parcelable::class.java.isAssignableFrom(componentType) -> { | ||||||
|  |                         arguments.getParcelableArray(key) | ||||||
|  |                     } | ||||||
|  |                     String::class.java.isAssignableFrom(componentType) -> { | ||||||
|  |                         arguments.getStringArray(key) | ||||||
|  |                     } | ||||||
|  |                     CharSequence::class.java.isAssignableFrom(componentType) -> { | ||||||
|  |                         arguments.getCharSequenceArray(key) | ||||||
|  |                     } | ||||||
|  |                     Serializable::class.java.isAssignableFrom(componentType) -> { | ||||||
|  |                         arguments.getSerializable(key) | ||||||
|  |                     } | ||||||
|  |                     else -> { | ||||||
|  |                         val valueType = componentType.canonicalName | ||||||
|  |                         throw IllegalArgumentException( | ||||||
|  |                             "Illegal value array type $valueType for key \"$key\"" | ||||||
|  |                         ) | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             Serializable::class -> arguments.getSerializable(key) | ||||||
|  |             else -> null | ||||||
|  |         } as? T | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | object ArgumentPropertyNonNullableDelegate { | ||||||
|  |     operator fun <T: Any> getValue(thisRef: Fragment, property: KProperty<*>): T { | ||||||
|  |         return ArgumentPropertyNullableDelegate.getValue<T>(thisRef, property)!! | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fun argumentOrNull() = ArgumentPropertyNullableDelegate | ||||||
|  | fun argumentOrThrow() = ArgumentPropertyNonNullableDelegate | ||||||
| @@ -0,0 +1,13 @@ | |||||||
|  | @file:Suppress("NOTHING_TO_INLINE") | ||||||
|  |  | ||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | import android.content.res.Resources | ||||||
|  |  | ||||||
|  | inline fun Resources.getSp( | ||||||
|  |     resId: Int | ||||||
|  | ) = getDimension(resId) / displayMetrics.scaledDensity | ||||||
|  |  | ||||||
|  | inline fun Resources.getDp( | ||||||
|  |     resId: Int | ||||||
|  | ) = getDimension(resId) * displayMetrics.density | ||||||
| @@ -0,0 +1,140 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | import android.view.View | ||||||
|  | import android.view.ViewGroup | ||||||
|  | import android.view.animation.Animation | ||||||
|  | import android.view.animation.Transformation | ||||||
|  |  | ||||||
|  | private fun View.performExpand( | ||||||
|  |     duration: Long = 500, | ||||||
|  |     targetWidth: Int = ViewGroup.LayoutParams.MATCH_PARENT, | ||||||
|  |     targetHeight: Int = ViewGroup.LayoutParams.WRAP_CONTENT, | ||||||
|  |     onMeasured: View.() -> Unit, | ||||||
|  |     onPerformAnimation: View.(interpolatedTime: Float, t: Transformation?) -> Unit | ||||||
|  | ) { | ||||||
|  |     measure(targetWidth, targetHeight) | ||||||
|  |     onMeasured() | ||||||
|  |     show() | ||||||
|  |     val a: Animation = object : Animation() { | ||||||
|  |         override fun applyTransformation(interpolatedTime: Float, t: Transformation?) { | ||||||
|  |             super.applyTransformation(interpolatedTime, t) | ||||||
|  |             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 | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     a.duration = duration | ||||||
|  |  | ||||||
|  |     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 | ||||||
|  |     performCollapse(duration) { interpolatedTime, _ -> | ||||||
|  |         layoutParams.height = initialHeight - (initialHeight * interpolatedTime).toInt() | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @PreviewFeature | ||||||
|  | fun View.collapseHorizontally(duration: Long = 500) { | ||||||
|  |     val initialWidth: Int = measuredWidth | ||||||
|  |     performCollapse(duration) { interpolatedTime, _ -> | ||||||
|  |         layoutParams.width = initialWidth - (initialWidth * interpolatedTime).toInt() | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @PreviewFeature | ||||||
|  | inline val View.isCollapsed | ||||||
|  |     get() = visibility == View.GONE | ||||||
|  |  | ||||||
|  | @PreviewFeature | ||||||
|  | inline val View.isExpanded | ||||||
|  |     get() = !isCollapsed | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @return true in case of expanding | ||||||
|  |  */ | ||||||
|  | @PreviewFeature | ||||||
|  | fun View.toggleExpandState(duration: Long = 500): Boolean = if (isCollapsed) { | ||||||
|  |     expand(duration) | ||||||
|  |     true | ||||||
|  | } else { | ||||||
|  |     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 | ||||||
|  |  | ||||||
|  | @Suppress("UNCHECKED_CAST", "SimplifiableCall") | ||||||
|  | inline fun <T, R> Iterable<T>.mapNotNullA(transform: (T) -> R?): List<R> = map(transform).filter { it != null } as List<R> | ||||||
|  |  | ||||||
|  | @Suppress("UNCHECKED_CAST", "SimplifiableCall") | ||||||
|  | inline fun <T, R> Array<T>.mapNotNullA(mapper: (T) -> R?): List<R> = map(mapper).filter { it != null } as List<R> | ||||||
| @@ -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 | ||||||
| @@ -0,0 +1,34 @@ | |||||||
|  | @file:Suppress("NOTHING_TO_INLINE", "unused") | ||||||
|  |  | ||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | import android.view.View | ||||||
|  | import android.view.ViewGroup | ||||||
|  |  | ||||||
|  | inline val View.enabled | ||||||
|  |     get() = isEnabled | ||||||
|  |  | ||||||
|  | inline val View.disabled | ||||||
|  |     get() = !enabled | ||||||
|  |  | ||||||
|  | fun View.disable() { | ||||||
|  |     if (this is ViewGroup) { | ||||||
|  |         (0 until childCount).forEach { getChildAt(it).disable() } | ||||||
|  |     } | ||||||
|  |     isEnabled = false | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fun View.enable() { | ||||||
|  |     if (this is ViewGroup) { | ||||||
|  |         (0 until childCount).forEach { getChildAt(it).enable() } | ||||||
|  |     } | ||||||
|  |     isEnabled = true | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fun View.toggleEnabledState(enabled: Boolean) { | ||||||
|  |     if (enabled) { | ||||||
|  |         enable() | ||||||
|  |     } else { | ||||||
|  |         disable() | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,47 @@ | |||||||
|  | @file:Suppress("NOTHING_TO_INLINE", "unused") | ||||||
|  |  | ||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | import android.view.View | ||||||
|  |  | ||||||
|  | inline val View.gone | ||||||
|  |     get() = visibility == View.GONE | ||||||
|  | inline fun View.gone() { | ||||||
|  |     visibility = View.GONE | ||||||
|  | } | ||||||
|  |  | ||||||
|  | inline val View.hidden | ||||||
|  |     get() = visibility == View.INVISIBLE | ||||||
|  | inline fun View.hide() { | ||||||
|  |     visibility = View.INVISIBLE | ||||||
|  | } | ||||||
|  |  | ||||||
|  | inline val View.shown | ||||||
|  |     get() = visibility == View.VISIBLE | ||||||
|  | inline fun View.show() { | ||||||
|  |     visibility = View.VISIBLE | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fun View.toggleVisibility(goneOnHide: Boolean = true) { | ||||||
|  |     if (isShown) { | ||||||
|  |         if (goneOnHide) { | ||||||
|  |             gone() | ||||||
|  |         } else { | ||||||
|  |             hide() | ||||||
|  |         } | ||||||
|  |     } else { | ||||||
|  |         show() | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fun View.changeVisibility(show: Boolean = !isShown, goneOnHide: Boolean = true) { | ||||||
|  |     if (show) { | ||||||
|  |         show() | ||||||
|  |     } else { | ||||||
|  |         if (goneOnHide) { | ||||||
|  |             gone() | ||||||
|  |         } else { | ||||||
|  |             hide() | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,35 @@ | |||||||
|  | @file:Suppress("OPT_IN_IS_NOT_ENABLED") | ||||||
|  |  | ||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | @RequiresOptIn( | ||||||
|  |     "It is possible, that behaviour of this thing will be changed or removed in future releases", | ||||||
|  |     RequiresOptIn.Level.WARNING | ||||||
|  | ) | ||||||
|  | @Target( | ||||||
|  |     AnnotationTarget.CLASS, | ||||||
|  |     AnnotationTarget.CONSTRUCTOR, | ||||||
|  |     AnnotationTarget.FIELD, | ||||||
|  |     AnnotationTarget.PROPERTY, | ||||||
|  |     AnnotationTarget.PROPERTY_GETTER, | ||||||
|  |     AnnotationTarget.PROPERTY_SETTER, | ||||||
|  |     AnnotationTarget.FUNCTION, | ||||||
|  |     AnnotationTarget.TYPEALIAS | ||||||
|  | ) | ||||||
|  | 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) | ||||||
| @@ -0,0 +1,13 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | fun <T> List<T>.breakAsPairs(): List<Pair<T, T>> { | ||||||
|  |     val result = mutableListOf<Pair<T, T>>() | ||||||
|  |  | ||||||
|  |     for (i in 0 until size - 1) { | ||||||
|  |         val first = get(i) | ||||||
|  |         val second = get(i + 1) | ||||||
|  |         result.add(first to second) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return result | ||||||
|  | } | ||||||
| @@ -0,0 +1,6 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Creates simple [Comparator] which will use [compareTo] of [T] for both objects | ||||||
|  |  */ | ||||||
|  | fun <T : Comparable<C>, C : T> T.createComparator() = Comparator<C> { o1, o2 -> o1.compareTo(o2) } | ||||||
| @@ -0,0 +1,25 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | import korlibs.time.DateTime | ||||||
|  | import kotlinx.serialization.KSerializer | ||||||
|  | import kotlinx.serialization.Serializer | ||||||
|  | import kotlinx.serialization.builtins.serializer | ||||||
|  | import kotlinx.serialization.descriptors.SerialDescriptor | ||||||
|  | import kotlinx.serialization.encoding.Decoder | ||||||
|  | import kotlinx.serialization.encoding.Encoder | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Serializes [DateTime] as its raw [DateTime.unixMillis] and deserializes in the same way | ||||||
|  |  */ | ||||||
|  | object DateTimeSerializer : KSerializer<DateTime> { | ||||||
|  |     override val descriptor: SerialDescriptor | ||||||
|  |         get() = Double.serializer().descriptor | ||||||
|  |  | ||||||
|  |     override fun deserialize(decoder: Decoder): DateTime { | ||||||
|  |         return DateTime(decoder.decodeDouble()) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun serialize(encoder: Encoder, value: DateTime) { | ||||||
|  |         encoder.encodeDouble(value.unixMillis) | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -2,11 +2,13 @@ | |||||||
|  |  | ||||||
| package dev.inmo.micro_utils.common | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | import kotlinx.serialization.Serializable | ||||||
|  |  | ||||||
| private inline fun <T> getObject( | private inline fun <T> getObject( | ||||||
|     additional: MutableList<T>, |     additional: MutableList<T>, | ||||||
|     iterator: Iterator<T> |     iterator: Iterator<T> | ||||||
| ): T? = when { | ): T? = when { | ||||||
|     additional.isNotEmpty() -> additional.removeFirst() |     additional.isNotEmpty() -> additional.removeAt(0) | ||||||
|     iterator.hasNext() -> iterator.next() |     iterator.hasNext() -> iterator.next() | ||||||
|     else -> null |     else -> null | ||||||
| } | } | ||||||
| @@ -14,35 +16,53 @@ private inline fun <T> getObject( | |||||||
| /** | /** | ||||||
|  * Diff object which contains information about differences between two [Iterable]s |  * Diff object which contains information about differences between two [Iterable]s | ||||||
|  * |  * | ||||||
|  |  * See tests for more info | ||||||
|  |  * | ||||||
|  |  * @param removed The objects which has been presented in the old collection but absent in new one. Index here is the index in the old collection | ||||||
|  |  * @param added The object which appear in new collection only. Indexes here show the index in the new collection | ||||||
|  |  * @param replaced Pair of old-new changes. First object has been presented in the old collection on its | ||||||
|  |  * [IndexedValue.index] place, the second one is the object in new collection. Both have indexes due to the fact that in | ||||||
|  |  * case when some value has been replaced after adds or removes in original collection the object index will be changed | ||||||
|  |  * | ||||||
|  * @see calculateDiff |  * @see calculateDiff | ||||||
|  */ |  */ | ||||||
| data class Diff<T> internal constructor( | @Serializable | ||||||
|     val removed: List<IndexedValue<T>>, | data class Diff<T> @Warning(warning) constructor( | ||||||
|  |     val removed: List<@Serializable(IndexedValueSerializer::class) IndexedValue<T>>, | ||||||
|     /** |     /** | ||||||
|      * Old-New values pairs |      * Old-New values pairs | ||||||
|      */ |      */ | ||||||
|     val replaced: List<Pair<IndexedValue<T>, IndexedValue<T>>>, |     val replaced: List<Pair<@Serializable(IndexedValueSerializer::class) IndexedValue<T>, @Serializable(IndexedValueSerializer::class) IndexedValue<T>>>, | ||||||
|     val added: List<IndexedValue<T>> |     val added: List<@Serializable(IndexedValueSerializer::class) IndexedValue<T>> | ||||||
| ) | ) { | ||||||
|  |     fun isEmpty(): Boolean = removed.isEmpty() && replaced.isEmpty() && added.isEmpty() | ||||||
|  |  | ||||||
|  |     companion object { | ||||||
|  |         private const val warning = "This feature can be changed without any warranties. Use with caution and only in case you know what you are doing" | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fun <T> emptyDiff(): Diff<T> = Diff(emptyList(), emptyList(), emptyList()) | ||||||
|  |  | ||||||
| private inline fun <T> performChanges( | private inline fun <T> performChanges( | ||||||
|     potentialChanges: MutableList<Pair<IndexedValue<T>?, IndexedValue<T>?>>, |     potentialChanges: MutableList<Pair<IndexedValue<T>?, IndexedValue<T>?>>, | ||||||
|     additionalsInOld: MutableList<T>, |     additionsInOld: MutableList<T>, | ||||||
|     additionalsInNew: MutableList<T>, |     additionsInNew: MutableList<T>, | ||||||
|     changedList: MutableList<Pair<IndexedValue<T>, IndexedValue<T>>>, |     changedList: MutableList<Pair<IndexedValue<T>, IndexedValue<T>>>, | ||||||
|     removedList: MutableList<IndexedValue<T>>, |     removedList: MutableList<IndexedValue<T>>, | ||||||
|     addedList: MutableList<IndexedValue<T>>, |     addedList: MutableList<IndexedValue<T>>, | ||||||
|     strictComparison: Boolean |     comparisonFun: (T?, T?) -> Boolean | ||||||
| ) { | ) { | ||||||
|     var i = -1 |     var i = -1 | ||||||
|     val (oldObject, newObject) = potentialChanges.lastOrNull() ?: return |     val (oldObject, newObject) = potentialChanges.lastOrNull() ?: return | ||||||
|     for ((old, new) in potentialChanges.take(potentialChanges.size - 1)) { |     for ((old, new) in potentialChanges.take(potentialChanges.size - 1)) { | ||||||
|         i++ |         i++ | ||||||
|         val oldOneEqualToNewObject = old ?.value === newObject ?.value || (old ?.value == newObject ?.value && !strictComparison) |         val oldOneEqualToNewObject = comparisonFun(old ?.value, newObject ?.value) | ||||||
|         val newOneEqualToOldObject = new ?.value === oldObject ?.value || (new ?.value == oldObject ?.value && !strictComparison) |         val newOneEqualToOldObject = comparisonFun(new ?.value, oldObject ?.value) | ||||||
|         if (oldOneEqualToNewObject || newOneEqualToOldObject) { |         if (oldOneEqualToNewObject || newOneEqualToOldObject) { | ||||||
|             changedList.addAll( |             changedList.addAll( | ||||||
|                 potentialChanges.take(i).mapNotNull { |                 potentialChanges.take(i).mapNotNull { | ||||||
|  |                     @Suppress("UNCHECKED_CAST") | ||||||
|                     if (it.first != null && it.second != null) it as Pair<IndexedValue<T>, IndexedValue<T>> else null |                     if (it.first != null && it.second != null) it as Pair<IndexedValue<T>, IndexedValue<T>> else null | ||||||
|                 } |                 } | ||||||
|             ) |             ) | ||||||
| @@ -52,20 +72,20 @@ private inline fun <T> performChanges( | |||||||
|                     newPotentials.first().second ?.let { addedList.add(it) } |                     newPotentials.first().second ?.let { addedList.add(it) } | ||||||
|                     newPotentials.drop(1).take(newPotentials.size - 2).forEach { (oldOne, newOne) -> |                     newPotentials.drop(1).take(newPotentials.size - 2).forEach { (oldOne, newOne) -> | ||||||
|                         addedList.add(newOne!!) |                         addedList.add(newOne!!) | ||||||
|                         oldOne ?.let { additionalsInOld.add(oldOne.value) } |                         oldOne ?.let { additionsInOld.add(oldOne.value) } | ||||||
|                     } |                     } | ||||||
|                     if (newPotentials.size > 1) { |                     if (newPotentials.size > 1) { | ||||||
|                         newPotentials.last().first ?.value ?.let { additionalsInOld.add(it) } |                         newPotentials.last().first ?.value ?.let { additionsInOld.add(it) } | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|                 newOneEqualToOldObject -> { |                 newOneEqualToOldObject -> { | ||||||
|                     newPotentials.first().first ?.let { removedList.add(it) } |                     newPotentials.first().first ?.let { removedList.add(it) } | ||||||
|                     newPotentials.drop(1).take(newPotentials.size - 2).forEach { (oldOne, newOne) -> |                     newPotentials.drop(1).take(newPotentials.size - 2).forEach { (oldOne, newOne) -> | ||||||
|                         removedList.add(oldOne!!) |                         removedList.add(oldOne!!) | ||||||
|                         newOne ?.let { additionalsInNew.add(newOne.value) } |                         newOne ?.let { additionsInNew.add(newOne.value) } | ||||||
|                     } |                     } | ||||||
|                     if (newPotentials.size > 1) { |                     if (newPotentials.size > 1) { | ||||||
|                         newPotentials.last().second ?.value ?.let { additionalsInNew.add(it) } |                         newPotentials.last().second ?.value ?.let { additionsInNew.add(it) } | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
| @@ -92,7 +112,7 @@ private inline fun <T> performChanges( | |||||||
|  */ |  */ | ||||||
| fun <T> Iterable<T>.calculateDiff( | fun <T> Iterable<T>.calculateDiff( | ||||||
|     other: Iterable<T>, |     other: Iterable<T>, | ||||||
|     strictComparison: Boolean = false |     comparisonFun: (T?, T?) -> Boolean | ||||||
| ): Diff<T> { | ): Diff<T> { | ||||||
|     var i = -1 |     var i = -1 | ||||||
|     var j = -1 |     var j = -1 | ||||||
| @@ -120,27 +140,60 @@ fun <T> Iterable<T>.calculateDiff( | |||||||
|         } |         } | ||||||
|  |  | ||||||
|         when { |         when { | ||||||
|             oldObject === newObject || (oldObject == newObject && !strictComparison) -> { |             comparisonFun(oldObject, newObject) -> { | ||||||
|                 changedObjects.addAll(potentiallyChangedObjects.map { it as Pair<IndexedValue<T>, IndexedValue<T>> }) |                 changedObjects.addAll(potentiallyChangedObjects.map { | ||||||
|  |                     @Suppress("UNCHECKED_CAST") | ||||||
|  |                     it as Pair<IndexedValue<T>, IndexedValue<T>> | ||||||
|  |                 }) | ||||||
|                 potentiallyChangedObjects.clear() |                 potentiallyChangedObjects.clear() | ||||||
|             } |             } | ||||||
|             else -> { |             else -> { | ||||||
|                 potentiallyChangedObjects.add(oldObject ?.let { IndexedValue(i, oldObject) } to newObject ?.let { IndexedValue(j, newObject) }) |                 potentiallyChangedObjects.add(oldObject ?.let { IndexedValue(i, oldObject) } to newObject ?.let { IndexedValue(j, newObject) }) | ||||||
|                 val previousOldsAdditionsSize = additionalInOld.size |                 val previousOldsAdditionsSize = additionalInOld.size | ||||||
|                 val previousNewsAdditionsSize = additionalInNew.size |                 val previousNewsAdditionsSize = additionalInNew.size | ||||||
|                 performChanges(potentiallyChangedObjects, additionalInOld, additionalInNew, changedObjects, removedObjects, addedObjects, strictComparison) |                 performChanges(potentiallyChangedObjects, additionalInOld, additionalInNew, changedObjects, removedObjects, addedObjects, comparisonFun) | ||||||
|                 i -= (additionalInOld.size - previousOldsAdditionsSize) |                 i -= (additionalInOld.size - previousOldsAdditionsSize) | ||||||
|                 j -= (additionalInNew.size - previousNewsAdditionsSize) |                 j -= (additionalInNew.size - previousNewsAdditionsSize) | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|     potentiallyChangedObjects.add(null to null) |     potentiallyChangedObjects.add(null to null) | ||||||
|     performChanges(potentiallyChangedObjects, additionalInOld, additionalInNew, changedObjects, removedObjects, addedObjects, strictComparison) |     performChanges(potentiallyChangedObjects, additionalInOld, additionalInNew, changedObjects, removedObjects, addedObjects, comparisonFun) | ||||||
|  |  | ||||||
|     return Diff(removedObjects.toList(), changedObjects.toList(), addedObjects.toList()) |     return Diff(removedObjects.toList(), changedObjects.toList(), addedObjects.toList()) | ||||||
| } | } | ||||||
|  |  | ||||||
| inline fun <T> Diff(old: Iterable<T>, new: Iterable<T>) = old.calculateDiff(new) | /** | ||||||
|  |  * Calculating [Diff] object | ||||||
|  |  * | ||||||
|  |  * @param strictComparison If this parameter set to true, objects which are not equal by links will be used as different | ||||||
|  |  * objects. For example, in case of two "Example" string they will be equal by value, but CAN be different by links | ||||||
|  |  */ | ||||||
|  | fun <T> Iterable<T>.calculateDiff( | ||||||
|  |     other: Iterable<T>, | ||||||
|  |     strictComparison: Boolean = false | ||||||
|  | ): Diff<T> = calculateDiff( | ||||||
|  |     other, | ||||||
|  |     comparisonFun = if (strictComparison) { | ||||||
|  |         { t1, t2 -> | ||||||
|  |             t1 === t2 | ||||||
|  |         } | ||||||
|  |     } else { | ||||||
|  |         { t1, t2 -> | ||||||
|  |             t1 === t2 || t1 == t2 // small optimization for cases when t1 and t2 are the same - comparison will be faster potentially | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | ) | ||||||
|  | inline fun <T> Iterable<T>.diff( | ||||||
|  |     other: Iterable<T>, | ||||||
|  |     strictComparison: Boolean = false | ||||||
|  | ): Diff<T> = calculateDiff(other, strictComparison) | ||||||
|  | inline fun <T> Iterable<T>.diff( | ||||||
|  |     other: Iterable<T>, | ||||||
|  |     noinline comparisonFun: (T?, T?) -> Boolean | ||||||
|  | ): Diff<T> = calculateDiff(other, comparisonFun) | ||||||
|  |  | ||||||
|  | inline fun <T> Diff(old: Iterable<T>, new: Iterable<T>) = old.calculateDiff(new, strictComparison = false) | ||||||
| inline fun <T> StrictDiff(old: Iterable<T>, new: Iterable<T>) = old.calculateDiff(new, true) | inline fun <T> StrictDiff(old: Iterable<T>, new: Iterable<T>) = old.calculateDiff(new, true) | ||||||
|  |  | ||||||
| /** | /** | ||||||
| @@ -151,16 +204,50 @@ inline fun <T> Iterable<T>.calculateStrictDiff( | |||||||
| ) = calculateDiff(other, strictComparison = true) | ) = calculateDiff(other, strictComparison = true) | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Compare one-to-one |  * Applies [diff] to [this] [MutableList] | ||||||
|  */ |  */ | ||||||
| @Deprecated("Will be removed or replaced with some new function. Use calculateDiff instead") | fun <T> MutableList<T>.applyDiff( | ||||||
| inline fun <T> Iterable<T>.syncWith( |     diff: Diff<T> | ||||||
|     other: Iterable<T>, |  | ||||||
|     noinline removed: (List<T>) -> Unit = {}, |  | ||||||
|     noinline added: (List<T>) -> Unit = {} |  | ||||||
| ) { | ) { | ||||||
|     calculateDiff(other).also { |     for (i in diff.removed.indices.sortedDescending()) { | ||||||
|         removed(it.removed.map { it.value }) |         removeAt(diff.removed[i].index) | ||||||
|         added(it.added.map { it.value }) |     } | ||||||
|  |     diff.added.forEach { (i, t) -> | ||||||
|  |         add(i, t) | ||||||
|  |     } | ||||||
|  |     diff.replaced.forEach { (_, new) -> | ||||||
|  |         set(new.index, new.value) | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * This method call [calculateDiff] with strict mode [strictComparison] and then apply differences to [this] | ||||||
|  |  * mutable list | ||||||
|  |  */ | ||||||
|  | fun <T> MutableList<T>.applyDiff( | ||||||
|  |     source: Iterable<T>, | ||||||
|  |     strictComparison: Boolean = false | ||||||
|  | ): Diff<T> = calculateDiff(source, strictComparison).also { | ||||||
|  |     applyDiff(it) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * This method call [calculateDiff] and then apply differences to [this] | ||||||
|  |  * mutable list | ||||||
|  |  */ | ||||||
|  | fun <T> MutableList<T>.applyDiff( | ||||||
|  |     source: Iterable<T>, | ||||||
|  |     comparisonFun: (T?, T?) -> Boolean | ||||||
|  | ): Diff<T> = calculateDiff(source, comparisonFun).also { | ||||||
|  |     applyDiff(it) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Reverse [this] [Diff]. Result will contain [Diff.added] on [Diff.removed] (and vice-verse), all the | ||||||
|  |  * [Diff.replaced] values will be reversed too | ||||||
|  |  */ | ||||||
|  | fun <T> Diff<T>.reversed() = Diff( | ||||||
|  |     removed = added, | ||||||
|  |     replaced = replaced.map { it.second to it.first }, | ||||||
|  |     added = removed | ||||||
|  | ) | ||||||
|   | |||||||
| @@ -0,0 +1,160 @@ | |||||||
|  | @file:Suppress("unused", "NOTHING_TO_INLINE") | ||||||
|  |  | ||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | import kotlinx.serialization.* | ||||||
|  | import kotlinx.serialization.builtins.serializer | ||||||
|  | import kotlinx.serialization.descriptors.* | ||||||
|  | import kotlinx.serialization.encoding.* | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Realization of this interface will contains at least one not null - [optionalT1] or [optionalT2] | ||||||
|  |  * | ||||||
|  |  * @see EitherFirst | ||||||
|  |  * @see EitherSecond | ||||||
|  |  * @see Either.Companion.first | ||||||
|  |  * @see Either.Companion.second | ||||||
|  |  * @see Either.onFirst | ||||||
|  |  * @see Either.onSecond | ||||||
|  |  * @see Either.mapOnFirst | ||||||
|  |  * @see Either.mapOnSecond | ||||||
|  |  */ | ||||||
|  | @Serializable(EitherSerializer::class) | ||||||
|  | sealed interface Either<T1, T2> { | ||||||
|  |     val optionalT1: Optional<T1> | ||||||
|  |     val optionalT2: Optional<T2> | ||||||
|  |  | ||||||
|  |     val t1OrNull: T1? | ||||||
|  |         get() = optionalT1.dataOrNull() | ||||||
|  |     val t2OrNull: T2? | ||||||
|  |         get() = optionalT2.dataOrNull() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class EitherSerializer<T1, T2>( | ||||||
|  |     t1Serializer: KSerializer<T1>, | ||||||
|  |     t2Serializer: KSerializer<T2>, | ||||||
|  | ) : KSerializer<Either<T1, T2>> { | ||||||
|  |     @OptIn(InternalSerializationApi::class, ExperimentalSerializationApi::class) | ||||||
|  |     override val descriptor: SerialDescriptor = buildSerialDescriptor( | ||||||
|  |         "TypedSerializer", | ||||||
|  |         SerialKind.CONTEXTUAL | ||||||
|  |     ) { | ||||||
|  |         element("type", String.serializer().descriptor) | ||||||
|  |         element("value", ContextualSerializer(Either::class).descriptor) | ||||||
|  |     } | ||||||
|  |     private val t1EitherSerializer = EitherFirst.serializer(t1Serializer, t2Serializer) | ||||||
|  |     private val t2EitherSerializer = EitherSecond.serializer(t1Serializer, t2Serializer) | ||||||
|  |  | ||||||
|  |     override fun deserialize(decoder: Decoder): Either<T1, T2> { | ||||||
|  |         return decoder.decodeStructure(descriptor) { | ||||||
|  |             var type: String? = null | ||||||
|  |             lateinit var result: Either<T1, T2> | ||||||
|  |             while (true) { | ||||||
|  |                 when (val index = decodeElementIndex(descriptor)) { | ||||||
|  |                     0 -> type = decodeStringElement(descriptor, 0) | ||||||
|  |                     1 -> { | ||||||
|  |                         result = when (type) { | ||||||
|  |                             "t1" -> decodeSerializableElement( | ||||||
|  |                                 descriptor, | ||||||
|  |                                 1, | ||||||
|  |                                 t1EitherSerializer | ||||||
|  |                             ) | ||||||
|  |                             "t2" -> decodeSerializableElement( | ||||||
|  |                                 descriptor, | ||||||
|  |                                 1, | ||||||
|  |                                 t2EitherSerializer | ||||||
|  |                             ) | ||||||
|  |                             else -> error("Unknown type of either: $type") | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                     CompositeDecoder.DECODE_DONE -> break | ||||||
|  |                     else -> error("Unexpected index: $index") | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             result | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     override fun serialize(encoder: Encoder, value: Either<T1, T2>) { | ||||||
|  |         encoder.encodeStructure(descriptor) { | ||||||
|  |             when (value) { | ||||||
|  |                 is EitherFirst -> { | ||||||
|  |                     encodeStringElement(descriptor, 0, "t1") | ||||||
|  |                     encodeSerializableElement(descriptor, 1, t1EitherSerializer, value) | ||||||
|  |                 } | ||||||
|  |                 is EitherSecond -> { | ||||||
|  |                     encodeStringElement(descriptor, 0, "t2") | ||||||
|  |                     encodeSerializableElement(descriptor, 1, t2EitherSerializer, value) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * This type [Either] will always have not nullable [optionalT1] | ||||||
|  |  */ | ||||||
|  | @Serializable | ||||||
|  | data class EitherFirst<T1, T2>( | ||||||
|  |     val t1: T1 | ||||||
|  | ) : Either<T1, T2> { | ||||||
|  |     override val optionalT1: Optional<T1> = t1.optional | ||||||
|  |     override val optionalT2: Optional<T2> = Optional.absent() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * This type [Either] will always have not nullable [optionalT2] | ||||||
|  |  */ | ||||||
|  | @Serializable | ||||||
|  | data class EitherSecond<T1, T2>( | ||||||
|  |     val t2: T2 | ||||||
|  | ) : Either<T1, T2> { | ||||||
|  |     override val optionalT1: Optional<T1> = Optional.absent() | ||||||
|  |     override val optionalT2: Optional<T2> = t2.optional | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @return New instance of [EitherFirst] | ||||||
|  |  */ | ||||||
|  | inline fun <T1, T2> Either.Companion.first(t1: T1): Either<T1, T2> = EitherFirst(t1) | ||||||
|  | /** | ||||||
|  |  * @return New instance of [EitherSecond] | ||||||
|  |  */ | ||||||
|  | inline fun <T1, T2> Either.Companion.second(t2: T2): Either<T1, T2> = EitherSecond(t2) | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Will call [block] in case when [this] is [EitherFirst] | ||||||
|  |  */ | ||||||
|  | inline fun <T1, T2, E : Either<T1, T2>> E.onFirst(block: (T1) -> Unit): E { | ||||||
|  |     optionalT1.onPresented(block) | ||||||
|  |     return this | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Will call [block] in case when [this] is [EitherSecond] | ||||||
|  |  */ | ||||||
|  | inline fun <T1, T2, E : Either<T1, T2>> E.onSecond(block: (T2) -> Unit): E { | ||||||
|  |     optionalT2.onPresented(block) | ||||||
|  |     return this | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @return Result of [block] if [this] is [EitherFirst] | ||||||
|  |  */ | ||||||
|  | inline fun <T1, R> Either<T1, *>.mapOnFirst(block: (T1) -> R): R? { | ||||||
|  |     return optionalT1.mapOnPresented(block) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @return Result of [block] if [this] is [EitherSecond] | ||||||
|  |  */ | ||||||
|  | inline fun <T2, R> Either<*, T2>.mapOnSecond(block: (T2) -> R): R? { | ||||||
|  |     return optionalT2.mapOnPresented(block) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | inline fun <reified T1, reified T2> Any.either() = when (this) { | ||||||
|  |     is T1 -> Either.first<T1, T2>(this) | ||||||
|  |     is T2 -> Either.second<T1, T2>(this) | ||||||
|  |     else -> error("Incorrect type of either argument $this") | ||||||
|  | } | ||||||
| @@ -0,0 +1,43 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | inline fun <T> Boolean.letIfTrue(block: () -> T): T? { | ||||||
|  |     return if (this) { | ||||||
|  |         block() | ||||||
|  |     } else { | ||||||
|  |         null | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | inline fun <T> Boolean.letIfFalse(block: () -> T): T? { | ||||||
|  |     return if (this) { | ||||||
|  |         null | ||||||
|  |     } else { | ||||||
|  |         block() | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | inline fun Boolean.alsoIfTrue(block: () -> Unit): Boolean { | ||||||
|  |     letIfTrue(block) | ||||||
|  |     return this | ||||||
|  | } | ||||||
|  |  | ||||||
|  | inline fun Boolean.alsoIfFalse(block: () -> Unit): Boolean { | ||||||
|  |     letIfFalse(block) | ||||||
|  |     return this | ||||||
|  | } | ||||||
|  |  | ||||||
|  | inline fun <T> Boolean.ifTrue(block: () -> T): T? { | ||||||
|  |     return if (this) { | ||||||
|  |         block() | ||||||
|  |     } else { | ||||||
|  |         null | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | inline fun <T> Boolean.ifFalse(block: () -> T): T? { | ||||||
|  |     return if (this) { | ||||||
|  |         null | ||||||
|  |     } else { | ||||||
|  |         block() | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,30 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | import kotlinx.serialization.KSerializer | ||||||
|  | import kotlinx.serialization.Serializer | ||||||
|  | import kotlinx.serialization.builtins.PairSerializer | ||||||
|  | import kotlinx.serialization.builtins.serializer | ||||||
|  | import kotlinx.serialization.descriptors.SerialDescriptor | ||||||
|  | import kotlinx.serialization.encoding.Decoder | ||||||
|  | import kotlinx.serialization.encoding.Encoder | ||||||
|  |  | ||||||
|  | class IndexedValueSerializer<T>(private val subSerializer: KSerializer<T>) : KSerializer<IndexedValue<T>> { | ||||||
|  |     private val originalSerializer = PairSerializer(Int.serializer(), subSerializer) | ||||||
|  |     override val descriptor: SerialDescriptor | ||||||
|  |         get() = originalSerializer.descriptor | ||||||
|  |  | ||||||
|  |     override fun deserialize(decoder: Decoder): IndexedValue<T> { | ||||||
|  |         val pair = originalSerializer.deserialize(decoder) | ||||||
|  |         return IndexedValue( | ||||||
|  |             pair.first, | ||||||
|  |             pair.second | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun serialize(encoder: Encoder, value: IndexedValue<T>) { | ||||||
|  |         originalSerializer.serialize( | ||||||
|  |             encoder, | ||||||
|  |             Pair(value.index, value.value) | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -7,9 +7,17 @@ import kotlinx.serialization.encoding.Decoder | |||||||
| import kotlinx.serialization.encoding.Encoder | import kotlinx.serialization.encoding.Encoder | ||||||
|  |  | ||||||
| typealias ByteArrayAllocator = () -> ByteArray | typealias ByteArrayAllocator = () -> ByteArray | ||||||
|  | typealias SuspendByteArrayAllocator = suspend () -> ByteArray | ||||||
|  |  | ||||||
| val ByteArray.asAllocator: ByteArrayAllocator | val ByteArray.asAllocator: ByteArrayAllocator | ||||||
|     get() = { this } |     get() = { this } | ||||||
|  | val ByteArray.asSuspendAllocator: SuspendByteArrayAllocator | ||||||
|  |     get() = { this } | ||||||
|  | val ByteArrayAllocator.asSuspendAllocator: SuspendByteArrayAllocator | ||||||
|  |     get() = { this() } | ||||||
|  | suspend fun SuspendByteArrayAllocator.asAllocator(): ByteArrayAllocator { | ||||||
|  |     return invoke().asAllocator | ||||||
|  | } | ||||||
|  |  | ||||||
| object ByteArrayAllocatorSerializer : KSerializer<ByteArrayAllocator> { | object ByteArrayAllocatorSerializer : KSerializer<ByteArrayAllocator> { | ||||||
|     private val realSerializer = ByteArraySerializer() |     private val realSerializer = ByteArraySerializer() | ||||||
| @@ -17,7 +25,7 @@ object ByteArrayAllocatorSerializer : KSerializer<ByteArrayAllocator> { | |||||||
|  |  | ||||||
|     override fun deserialize(decoder: Decoder): ByteArrayAllocator { |     override fun deserialize(decoder: Decoder): ByteArrayAllocator { | ||||||
|         val bytes = realSerializer.deserialize(decoder) |         val bytes = realSerializer.deserialize(decoder) | ||||||
|         return { bytes } |         return bytes.asAllocator | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     override fun serialize(encoder: Encoder, value: ByteArrayAllocator) { |     override fun serialize(encoder: Encoder, value: ByteArrayAllocator) { | ||||||
|   | |||||||
| @@ -0,0 +1,3 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | fun <T> Iterable<T?>.firstNotNull() = first { it != null }!! | ||||||
| @@ -0,0 +1,59 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | inline fun <I, R> Iterable<I>.joinTo( | ||||||
|  |     separatorFun: (I) -> R?, | ||||||
|  |     prefix: R? = null, | ||||||
|  |     postfix: R? = null, | ||||||
|  |     transform: (I) -> R? | ||||||
|  | ): List<R> { | ||||||
|  |     val result = mutableListOf<R>() | ||||||
|  |     val iterator = iterator() | ||||||
|  |  | ||||||
|  |     prefix ?.let(result::add) | ||||||
|  |  | ||||||
|  |     while (iterator.hasNext()) { | ||||||
|  |         val element = iterator.next() | ||||||
|  |         result.add(transform(element) ?: continue) | ||||||
|  |  | ||||||
|  |         if (iterator.hasNext()) { | ||||||
|  |             result.add(separatorFun(element) ?: continue) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     postfix ?.let(result::add) | ||||||
|  |  | ||||||
|  |     return result | ||||||
|  | } | ||||||
|  |  | ||||||
|  | inline fun <I, R> Iterable<I>.joinTo( | ||||||
|  |     separator: R? = null, | ||||||
|  |     prefix: R? = null, | ||||||
|  |     postfix: R? = null, | ||||||
|  |     transform: (I) -> R? | ||||||
|  | ): List<R> = joinTo({ separator }, prefix, postfix, transform) | ||||||
|  |  | ||||||
|  | inline fun <I> Iterable<I>.joinTo( | ||||||
|  |     separatorFun: (I) -> I?, | ||||||
|  |     prefix: I? = null, | ||||||
|  |     postfix: I? = null | ||||||
|  | ): List<I> = joinTo<I, I>(separatorFun, prefix, postfix) { it } | ||||||
|  |  | ||||||
|  | inline fun <I> Iterable<I>.joinTo( | ||||||
|  |     separator: I? = null, | ||||||
|  |     prefix: I? = null, | ||||||
|  |     postfix: I? = null | ||||||
|  | ): List<I> = joinTo<I>({ separator }, prefix, postfix) | ||||||
|  |  | ||||||
|  | inline fun <I, reified R> Array<I>.joinTo( | ||||||
|  |     separatorFun: (I) -> R?, | ||||||
|  |     prefix: R? = null, | ||||||
|  |     postfix: R? = null, | ||||||
|  |     transform: (I) -> R? | ||||||
|  | ): Array<R> = asIterable().joinTo(separatorFun, prefix, postfix, transform).toTypedArray() | ||||||
|  |  | ||||||
|  | inline fun <I, reified R> Array<I>.joinTo( | ||||||
|  |     separator: R? = null, | ||||||
|  |     prefix: R? = null, | ||||||
|  |     postfix: R? = null, | ||||||
|  |     transform: (I) -> R? | ||||||
|  | ): Array<R> = asIterable().joinTo(separator, prefix, postfix, transform).toTypedArray() | ||||||
| @@ -0,0 +1,35 @@ | |||||||
|  | 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 != MPPFilePathSeparator } | ||||||
|  |     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 == MPPFilePathSeparator } | ||||||
|  |     override fun toString(): String = string | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | expect class MPPFile | ||||||
|  |  | ||||||
|  | expect val MPPFile.filename: FileName | ||||||
|  | expect val MPPFilePathSeparator: Char | ||||||
|  | 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,133 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Contains diff based on the comparison of objects with the same [K]. | ||||||
|  |  * | ||||||
|  |  * @param removed Contains map with keys removed from parent map | ||||||
|  |  * @param changed Contains map with keys values changed new map in comparison with old one | ||||||
|  |  * @param added Contains map with new keys and values | ||||||
|  |  */ | ||||||
|  | data class MapDiff<K, V> @Warning(warning) constructor( | ||||||
|  |     val removed: Map<K, V>, | ||||||
|  |     val changed: Map<K, Pair<V, V>>, | ||||||
|  |     val added: Map<K, V> | ||||||
|  | ) { | ||||||
|  |     fun isEmpty() = removed.isEmpty() && changed.isEmpty() && added.isEmpty() | ||||||
|  |     inline fun isNotEmpty() = !isEmpty() | ||||||
|  |  | ||||||
|  |     companion object { | ||||||
|  |         private const val warning = "This feature can be changed without any warranties. Use with caution and only in case you know what you are doing" | ||||||
|  |         fun <K, V> empty() = MapDiff<K, V>(emptyMap(), emptyMap(), emptyMap()) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | private inline fun <K, V> createCompareFun( | ||||||
|  |     strictComparison: Boolean | ||||||
|  | ): (K, V, V) -> Boolean = if (strictComparison) { | ||||||
|  |     { _, first, second -> first === second } | ||||||
|  | } else { | ||||||
|  |     { _, first, second -> first == second } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Compare [this] [Map] with the [other] one in principle when [other] is newer than [this] | ||||||
|  |  * | ||||||
|  |  * @param compareFun Will be used to determine changed values | ||||||
|  |  */ | ||||||
|  | fun <K, V> Map<K, V>.diff( | ||||||
|  |     other: Map<K, V>, | ||||||
|  |     compareFun: (K, V, V) -> Boolean | ||||||
|  | ): MapDiff<K, V> { | ||||||
|  |     val removed: Map<K, V> = (keys - other.keys).associateWith { | ||||||
|  |         getValue(it) | ||||||
|  |     } | ||||||
|  |     val added: Map<K, V> = (other.keys - keys).associateWith { | ||||||
|  |         other.getValue(it) | ||||||
|  |     } | ||||||
|  |     val changed = keys.intersect(other.keys).mapNotNull { | ||||||
|  |         val old = getValue(it) | ||||||
|  |         val new = other.getValue(it) | ||||||
|  |         if (compareFun(it, old, new)) { | ||||||
|  |             return@mapNotNull null | ||||||
|  |         } else { | ||||||
|  |             it to (old to new) | ||||||
|  |         } | ||||||
|  |     }.toMap() | ||||||
|  |  | ||||||
|  |     return MapDiff( | ||||||
|  |         removed, | ||||||
|  |         changed, | ||||||
|  |         added | ||||||
|  |     ) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Compare [this] [Map] with the [other] one in principle when [other] is newer than [this] | ||||||
|  |  * | ||||||
|  |  * @param strictComparison If true, will use strict (===) comparison for the values' comparison. Otherwise, standard | ||||||
|  |  * `equals` will be used | ||||||
|  |  */ | ||||||
|  | fun <K, V> Map<K, V>.diff( | ||||||
|  |     other: Map<K, V>, | ||||||
|  |     strictComparison: Boolean = false | ||||||
|  | ): MapDiff<K, V> = diff( | ||||||
|  |     other, | ||||||
|  |     compareFun = createCompareFun(strictComparison) | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Will apply [mapDiff] to [this] [MutableMap] | ||||||
|  |  */ | ||||||
|  | fun <K, V> MutableMap<K, V>.applyDiff( | ||||||
|  |     mapDiff: MapDiff<K, V> | ||||||
|  | ) { | ||||||
|  |     mapDiff.apply { | ||||||
|  |         keys.removeAll(removed.keys) | ||||||
|  |         changed.forEach { (k, oldNew) -> | ||||||
|  |             put(k, oldNew.second) | ||||||
|  |         } | ||||||
|  |         putAll(added) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Will apply changes with [from] map into [this] one | ||||||
|  |  * | ||||||
|  |  * @param compareFun Will be used to determine changed values | ||||||
|  |  * | ||||||
|  |  * @return [MapDiff] applied to [this] [MutableMap] | ||||||
|  |  */ | ||||||
|  | fun <K, V> MutableMap<K, V>.applyDiff( | ||||||
|  |     from: Map<K, V>, | ||||||
|  |     compareFun: (K, V, V) -> Boolean | ||||||
|  | ): MapDiff<K, V> { | ||||||
|  |     return diff(from, compareFun).also { | ||||||
|  |         applyDiff(it) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Will apply changes with [from] map into [this] one | ||||||
|  |  * | ||||||
|  |  * @param strictComparison If true, will use strict (===) comparison for the values' comparison. Otherwise, standard | ||||||
|  |  * `equals` will be used | ||||||
|  |  * | ||||||
|  |  * @return [MapDiff] applied to [this] [MutableMap] | ||||||
|  |  */ | ||||||
|  | fun <K, V> MutableMap<K, V>.applyDiff( | ||||||
|  |     from: Map<K, V>, | ||||||
|  |     strictComparison: Boolean = false | ||||||
|  | ): MapDiff<K, V> = applyDiff( | ||||||
|  |     from, | ||||||
|  |     compareFun = createCompareFun(strictComparison) | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Reverse [this] [MapDiff]. Result will contain [MapDiff.added] on [MapDiff.removed] (and vice-verse), all the | ||||||
|  |  * [MapDiff.changed] values will be reversed too | ||||||
|  |  */ | ||||||
|  | fun <K, V> MapDiff<K, V>.reversed(): MapDiff<K, V> = MapDiff( | ||||||
|  |     removed = added, | ||||||
|  |     changed = changed.mapValues { (_, oldNew) -> oldNew.second to oldNew.first }, | ||||||
|  |     added = removed | ||||||
|  | ) | ||||||
| @@ -0,0 +1,53 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | import kotlin.jvm.JvmName | ||||||
|  |  | ||||||
|  | interface SimpleMapper<T1, T2> { | ||||||
|  |     fun convertToT1(from: T2): T1 | ||||||
|  |     fun convertToT2(from: T1): T2 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @JvmName("convertFromT2") | ||||||
|  | fun <T1, T2> SimpleMapper<T1, T2>.convert(from: T2) = convertToT1(from) | ||||||
|  | @JvmName("convertFromT1") | ||||||
|  | fun <T1, T2> SimpleMapper<T1, T2>.convert(from: T1) = convertToT2(from) | ||||||
|  |  | ||||||
|  | class SimpleMapperImpl<T1, T2>( | ||||||
|  |     private val t1: (T2) -> T1, | ||||||
|  |     private val t2: (T1) -> T2, | ||||||
|  | ) : SimpleMapper<T1, T2> { | ||||||
|  |     override fun convertToT1(from: T2): T1 = t1.invoke(from) | ||||||
|  |  | ||||||
|  |     override fun convertToT2(from: T1): T2 = t2.invoke(from) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @Suppress("NOTHING_TO_INLINE") | ||||||
|  | inline fun <T1, T2> simpleMapper( | ||||||
|  |     noinline t1: (T2) -> T1, | ||||||
|  |     noinline t2: (T1) -> T2, | ||||||
|  | ) = SimpleMapperImpl(t1, t2) | ||||||
|  |  | ||||||
|  | interface SimpleSuspendableMapper<T1, T2> { | ||||||
|  |     suspend fun convertToT1(from: T2): T1 | ||||||
|  |     suspend fun convertToT2(from: T1): T2 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @JvmName("convertFromT2") | ||||||
|  | suspend fun <T1, T2> SimpleSuspendableMapper<T1, T2>.convert(from: T2) = convertToT1(from) | ||||||
|  | @JvmName("convertFromT1") | ||||||
|  | suspend fun <T1, T2> SimpleSuspendableMapper<T1, T2>.convert(from: T1) = convertToT2(from) | ||||||
|  |  | ||||||
|  | class SimpleSuspendableMapperImpl<T1, T2>( | ||||||
|  |     private val t1: suspend (T2) -> T1, | ||||||
|  |     private val t2: suspend (T1) -> T2, | ||||||
|  | ) : SimpleSuspendableMapper<T1, T2> { | ||||||
|  |     override suspend fun convertToT1(from: T2): T1 = t1.invoke(from) | ||||||
|  |  | ||||||
|  |     override suspend fun convertToT2(from: T1): T2 = t2.invoke(from) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @Suppress("NOTHING_TO_INLINE") | ||||||
|  | inline fun <T1, T2> simpleSuspendableMapper( | ||||||
|  |     noinline t1: suspend (T2) -> T1, | ||||||
|  |     noinline t2: suspend (T1) -> T2, | ||||||
|  | ) = SimpleSuspendableMapperImpl(t1, t2) | ||||||
| @@ -0,0 +1,97 @@ | |||||||
|  | @file:Suppress("unused") | ||||||
|  |  | ||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | import kotlinx.serialization.Serializable | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * This type represents [T] as not only potentially nullable data, but also as a data which can not be presented. This | ||||||
|  |  * type will be useful in cases when [T] is nullable and null as valuable data too in time of data absence should be | ||||||
|  |  * presented by some third type. | ||||||
|  |  * | ||||||
|  |  * Let's imagine, you have nullable name in some database. In case when name is not nullable everything is clear - null | ||||||
|  |  * will represent absence of row in the database. In case when name is nullable null will be a little bit dual-meaning, | ||||||
|  |  * cause this null will say nothing about availability of the row (of course, it is exaggerated example) | ||||||
|  |  * | ||||||
|  |  * @see Optional.presented | ||||||
|  |  * @see Optional.absent | ||||||
|  |  * @see Optional.optional | ||||||
|  |  * @see Optional.onPresented | ||||||
|  |  * @see Optional.onAbsent | ||||||
|  |  */ | ||||||
|  | @Serializable | ||||||
|  | data class Optional<T> internal constructor( | ||||||
|  |     @Warning("It is unsafe to use this data directly") | ||||||
|  |     val data: T?, | ||||||
|  |     @Warning("It is unsafe to use this data directly") | ||||||
|  |     val dataPresented: Boolean | ||||||
|  | ) { | ||||||
|  |     companion object { | ||||||
|  |         /** | ||||||
|  |          * Will create [Optional] with presented data | ||||||
|  |          */ | ||||||
|  |         fun <T> presented(data: T) = Optional(data, true) | ||||||
|  |         /** | ||||||
|  |          * Will create [Optional] without data | ||||||
|  |          */ | ||||||
|  |         fun <T> absent() = Optional<T>(null, false) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | inline val <T> T.optional | ||||||
|  |     get() = Optional.presented(this) | ||||||
|  |  | ||||||
|  | inline val <T : Any> T?.optionalOrAbsentIfNull | ||||||
|  |     get() = if (this == null) { | ||||||
|  |         Optional.absent<T>() | ||||||
|  |     } else { | ||||||
|  |         Optional.presented(this) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Will call [block] when data presented ([Optional.dataPresented] == true) | ||||||
|  |  */ | ||||||
|  | inline fun <T> Optional<T>.onPresented(block: (T) -> Unit): Optional<T> = apply { | ||||||
|  |     @OptIn(Warning::class) | ||||||
|  |     if (dataPresented) { @Suppress("UNCHECKED_CAST") block(data as T) } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Will call [block] when data presented ([Optional.dataPresented] == true) | ||||||
|  |  */ | ||||||
|  | inline fun <T, R> Optional<T>.mapOnPresented(block: (T) -> R): R? = run { | ||||||
|  |     @OptIn(Warning::class) | ||||||
|  |     if (dataPresented) { @Suppress("UNCHECKED_CAST") block(data as T) } else null | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Will call [block] when data absent ([Optional.dataPresented] == false) | ||||||
|  |  */ | ||||||
|  | inline fun <T> Optional<T>.onAbsent(block: () -> Unit): Optional<T> = apply { | ||||||
|  |     @OptIn(Warning::class) | ||||||
|  |     if (!dataPresented) { block() } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Will call [block] when data presented ([Optional.dataPresented] == true) | ||||||
|  |  */ | ||||||
|  | inline fun <T, R> Optional<T>.mapOnAbsent(block: () -> R): R? = run { | ||||||
|  |     @OptIn(Warning::class) | ||||||
|  |     if (!dataPresented) { block() } else null | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Returns [Optional.data] if [Optional.dataPresented] of [this] is true, or null otherwise | ||||||
|  |  */ | ||||||
|  | fun <T> Optional<T>.dataOrNull() = @OptIn(Warning::class) if (dataPresented) @Suppress("UNCHECKED_CAST") (data as T) else null | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Returns [Optional.data] if [Optional.dataPresented] of [this] is true, or throw [throwable] otherwise | ||||||
|  |  */ | ||||||
|  | fun <T> Optional<T>.dataOrThrow(throwable: Throwable) = @OptIn(Warning::class) if (dataPresented) @Suppress("UNCHECKED_CAST") (data as T) else throw throwable | ||||||
|  |  | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Returns [Optional.data] if [Optional.dataPresented] of [this] is true, or call [block] and returns the result of it | ||||||
|  |  */ | ||||||
|  | inline fun <T> Optional<T>.dataOrElse(block: () -> T) = @OptIn(Warning::class) if (dataPresented) @Suppress("UNCHECKED_CAST") (data as T) else block() | ||||||
| @@ -0,0 +1,32 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | inline fun <T> Sequence<T>.padWith(size: Int, inserter: (Sequence<T>) -> Sequence<T>): Sequence<T> { | ||||||
|  |     var result = this | ||||||
|  |     while (result.count() < size) { | ||||||
|  |         result = inserter(result) | ||||||
|  |     } | ||||||
|  |     return result | ||||||
|  | } | ||||||
|  |  | ||||||
|  | inline fun <T> Sequence<T>.padEnd(size: Int, padBlock: (Int) -> T): Sequence<T> = padWith(size) { it + padBlock(it.count()) } | ||||||
|  |  | ||||||
|  | inline fun <T> Sequence<T>.padEnd(size: Int, o: T) = padEnd(size) { o } | ||||||
|  |  | ||||||
|  | inline fun <T> List<T>.padWith(size: Int, inserter: (List<T>) -> List<T>): List<T> { | ||||||
|  |     var result = this | ||||||
|  |     while (result.size < size) { | ||||||
|  |         result = inserter(result) | ||||||
|  |     } | ||||||
|  |     return result | ||||||
|  | } | ||||||
|  | inline fun <T> List<T>.padEnd(size: Int, padBlock: (Int) -> T): List<T> = asSequence().padEnd(size, padBlock).toList() | ||||||
|  |  | ||||||
|  | inline fun <T> List<T>.padEnd(size: Int, o: T): List<T> = asSequence().padEnd(size, o).toList() | ||||||
|  |  | ||||||
|  | inline fun <T> Sequence<T>.padStart(size: Int, padBlock: (Int) -> T): Sequence<T> = padWith(size) { sequenceOf(padBlock(it.count())) + it } | ||||||
|  |  | ||||||
|  | inline fun <T> Sequence<T>.padStart(size: Int, o: T) = padStart(size) { o } | ||||||
|  |  | ||||||
|  | inline fun <T> List<T>.padStart(size: Int, padBlock: (Int) -> T): List<T> = asSequence().padStart(size, padBlock).toList() | ||||||
|  |  | ||||||
|  | inline fun <T> List<T>.padStart(size: Int, o: T): List<T> = asSequence().padStart(size, o).toList() | ||||||
| @@ -0,0 +1,76 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | import kotlinx.serialization.Serializable | ||||||
|  | import kotlin.jvm.JvmInline | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Contains [of1] as main value, where 100% of percentage is when of1 == 1 | ||||||
|  |  * | ||||||
|  |  * @see invoke | ||||||
|  |  * @see partOfTotal | ||||||
|  |  * @see of100 | ||||||
|  |  */ | ||||||
|  | @Serializable | ||||||
|  | @JvmInline | ||||||
|  | value class Percentage private constructor( | ||||||
|  |     /** | ||||||
|  |      * Value of percentage. When it equals to 1, means 100% | ||||||
|  |      */ | ||||||
|  |     val of1: Double | ||||||
|  | ) { | ||||||
|  |     /** | ||||||
|  |      * Same as [of1], but float (using [Double.toFloat]) | ||||||
|  |      */ | ||||||
|  |     val of1Float | ||||||
|  |         get() = of1.toFloat() | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Represent this percentage as common percentage where 100% is 100% | ||||||
|  |      */ | ||||||
|  |     val of100 | ||||||
|  |         get() = of1 * 100 | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Same as [of100], but float (using [Double.toFloat]) | ||||||
|  |      */ | ||||||
|  |     val of100Float | ||||||
|  |         get() = of100.toFloat() | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Same as [of100], but int (using [Double.toInt]) | ||||||
|  |      */ | ||||||
|  |     val of100Int | ||||||
|  |         get() = of100.toInt() | ||||||
|  |  | ||||||
|  |     companion object { | ||||||
|  |         val rangeOfValues = 0.0 .. 1.0 | ||||||
|  |  | ||||||
|  |         val START = Percentage(rangeOfValues.start) | ||||||
|  |         val COMPLETED = Percentage(rangeOfValues.endInclusive) | ||||||
|  |  | ||||||
|  |         operator fun invoke(of1: Double) = Percentage(of1.coerceIn(rangeOfValues)) | ||||||
|  |         operator fun invoke(part: Number, total: Number) = Percentage( | ||||||
|  |             part.toDouble() / total.toDouble() | ||||||
|  |         ) | ||||||
|  |         fun of1(of1: Double) = Percentage(of1 = of1) | ||||||
|  |         fun of100(of100: Double) = Percentage(of1 = of100 / 100) | ||||||
|  |         fun partOfTotal(part: Number, total: Number) = Percentage(part = part, total = total) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | typealias Progress = Percentage | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Will return [this] [Progress] if [Percentage.of1] in `0 .. 1` range | ||||||
|  |  */ | ||||||
|  | fun Progress.ensureStrictOrNull(): Progress? = if (of1 in Percentage.rangeOfValues) this else null | ||||||
|  | /** | ||||||
|  |  * Will return [this] [Progress] if [Percentage.of1] in `0 .. 1` range. Otherwise, will throw error | ||||||
|  |  * [IllegalArgumentException] due to [require] failure | ||||||
|  |  */ | ||||||
|  | fun Progress.ensureStrictOrThrow(): Progress { | ||||||
|  |     require(of1 in Percentage.rangeOfValues) { | ||||||
|  |         "For strict checks value of percentage must be in ${Percentage.rangeOfValues}, but actual value is $of1" | ||||||
|  |     } | ||||||
|  |     return this | ||||||
|  | } | ||||||
| @@ -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 Percentage.plus(other: Percentage): Percentage = Percentage(of1 + other.of1) | ||||||
|  |  | ||||||
|  | public operator fun Percentage.minus(other: Percentage): Percentage = Percentage(of1 - other.of1) | ||||||
|  |  | ||||||
|  | public operator fun Percentage.plus(i: Byte): Percentage = Percentage((of1 + i).toDouble()) | ||||||
|  |  | ||||||
|  | public operator fun Percentage.minus(i: Byte): Percentage = Percentage((of1 - i).toDouble()) | ||||||
|  |  | ||||||
|  | public operator fun Percentage.times(i: Byte): Percentage = Percentage((of1 * i).toDouble()) | ||||||
|  |  | ||||||
|  | public operator fun Percentage.div(i: Byte): Percentage = Percentage((of1 / i).toDouble()) | ||||||
|  |  | ||||||
|  | public operator fun Percentage.rem(i: Byte): Percentage = Percentage((of1 % i).toDouble()) | ||||||
|  |  | ||||||
|  | public operator fun Percentage.plus(i: Short): Percentage = Percentage((of1 + i).toDouble()) | ||||||
|  |  | ||||||
|  | public operator fun Percentage.minus(i: Short): Percentage = Percentage((of1 - i).toDouble()) | ||||||
|  |  | ||||||
|  | public operator fun Percentage.times(i: Short): Percentage = Percentage((of1 * i).toDouble()) | ||||||
|  |  | ||||||
|  | public operator fun Percentage.div(i: Short): Percentage = Percentage((of1 / i).toDouble()) | ||||||
|  |  | ||||||
|  | public operator fun Percentage.rem(i: Short): Percentage = Percentage((of1 % i).toDouble()) | ||||||
|  |  | ||||||
|  | public operator fun Percentage.plus(i: Int): Percentage = Percentage((of1 + i).toDouble()) | ||||||
|  |  | ||||||
|  | public operator fun Percentage.minus(i: Int): Percentage = Percentage((of1 - i).toDouble()) | ||||||
|  |  | ||||||
|  | public operator fun Percentage.times(i: Int): Percentage = Percentage((of1 * i).toDouble()) | ||||||
|  |  | ||||||
|  | public operator fun Percentage.div(i: Int): Percentage = Percentage((of1 / i).toDouble()) | ||||||
|  |  | ||||||
|  | public operator fun Percentage.rem(i: Int): Percentage = Percentage((of1 % i).toDouble()) | ||||||
|  |  | ||||||
|  | public operator fun Percentage.plus(i: Long): Percentage = Percentage((of1 + i).toDouble()) | ||||||
|  |  | ||||||
|  | public operator fun Percentage.minus(i: Long): Percentage = Percentage((of1 - i).toDouble()) | ||||||
|  |  | ||||||
|  | public operator fun Percentage.times(i: Long): Percentage = Percentage((of1 * i).toDouble()) | ||||||
|  |  | ||||||
|  | public operator fun Percentage.div(i: Long): Percentage = Percentage((of1 / i).toDouble()) | ||||||
|  |  | ||||||
|  | public operator fun Percentage.rem(i: Long): Percentage = Percentage((of1 % i).toDouble()) | ||||||
|  |  | ||||||
|  | public operator fun Percentage.plus(i: Float): Percentage = Percentage((of1 + i).toDouble()) | ||||||
|  |  | ||||||
|  | public operator fun Percentage.minus(i: Float): Percentage = Percentage((of1 - i).toDouble()) | ||||||
|  |  | ||||||
|  | public operator fun Percentage.times(i: Float): Percentage = Percentage((of1 * i).toDouble()) | ||||||
|  |  | ||||||
|  | public operator fun Percentage.div(i: Float): Percentage = Percentage((of1 / i).toDouble()) | ||||||
|  |  | ||||||
|  | public operator fun Percentage.rem(i: Float): Percentage = Percentage((of1 % i).toDouble()) | ||||||
|  |  | ||||||
|  | public operator fun Percentage.plus(i: Double): Percentage = Percentage((of1 + i).toDouble()) | ||||||
|  |  | ||||||
|  | public operator fun Percentage.minus(i: Double): Percentage = Percentage((of1 - i).toDouble()) | ||||||
|  |  | ||||||
|  | public operator fun Percentage.times(i: Double): Percentage = Percentage((of1 * i).toDouble()) | ||||||
|  |  | ||||||
|  | public operator fun Percentage.div(i: Double): Percentage = Percentage((of1 / i).toDouble()) | ||||||
|  |  | ||||||
|  | public operator fun Percentage.rem(i: Double): Percentage = Percentage((of1 % i).toDouble()) | ||||||
|  |  | ||||||
|  | public operator fun Percentage.compareTo(other: Percentage): Int = (of1.compareTo(other.of1)) | ||||||
| @@ -0,0 +1,28 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Convert [this] [Long] to [Int] with bounds of [Int.MIN_VALUE] and [Int.MAX_VALUE] | ||||||
|  |  */ | ||||||
|  | fun Long.toCoercedInt(): Int = coerceIn(Int.MIN_VALUE.toLong(), Int.MAX_VALUE.toLong()).toInt() | ||||||
|  | /** | ||||||
|  |  * Convert [this] [Long] to [Short] with bounds of [Short.MIN_VALUE] and [Short.MAX_VALUE] | ||||||
|  |  */ | ||||||
|  | fun Long.toCoercedShort(): Short = coerceIn(Short.MIN_VALUE.toLong(), Short.MAX_VALUE.toLong()).toShort() | ||||||
|  | /** | ||||||
|  |  * Convert [this] [Long] to [Byte] with bounds of [Byte.MIN_VALUE] and [Byte.MAX_VALUE] | ||||||
|  |  */ | ||||||
|  | fun Long.toCoercedByte(): Byte = coerceIn(Byte.MIN_VALUE.toLong(), Byte.MAX_VALUE.toLong()).toByte() | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Convert [this] [Int] to [Short] with bounds of [Short.MIN_VALUE] and [Short.MAX_VALUE] | ||||||
|  |  */ | ||||||
|  | fun Int.toCoercedShort(): Short = coerceIn(Short.MIN_VALUE.toInt(), Short.MAX_VALUE.toInt()).toShort() | ||||||
|  | /** | ||||||
|  |  * Convert [this] [Int] to [Byte] with bounds of [Byte.MIN_VALUE] and [Byte.MAX_VALUE] | ||||||
|  |  */ | ||||||
|  | fun Int.toCoercedByte(): Byte = coerceIn(Byte.MIN_VALUE.toInt(), Byte.MAX_VALUE.toInt()).toByte() | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Convert [this] [Short] to [Byte] with bounds of [Byte.MIN_VALUE] and [Byte.MAX_VALUE] | ||||||
|  |  */ | ||||||
|  | fun Short.toCoercedByte(): Byte = coerceIn(Byte.MIN_VALUE.toShort(), Byte.MAX_VALUE.toShort()).toByte() | ||||||
| @@ -0,0 +1,19 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | fun <T : Comparable<T>> ClosedRange<T>.intersect(other: ClosedRange<T>): Pair<T, T>? = when { | ||||||
|  |     start == other.start && endInclusive == other.endInclusive -> start to endInclusive | ||||||
|  |     start > other.endInclusive || other.start > endInclusive -> null | ||||||
|  |     else -> maxOf(start, other.start) to minOf(endInclusive, other.endInclusive) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fun IntRange.intersect( | ||||||
|  |     other: IntRange | ||||||
|  | ): IntRange? = (this as ClosedRange<Int>).intersect(other as ClosedRange<Int>) ?.let { | ||||||
|  |     it.first .. it.second | ||||||
|  | } | ||||||
|  |  | ||||||
|  | fun LongRange.intersect( | ||||||
|  |     other: LongRange | ||||||
|  | ): LongRange? = (this as ClosedRange<Long>).intersect(other as ClosedRange<Long>) ?.let { | ||||||
|  |     it.first .. it.second | ||||||
|  | } | ||||||
| @@ -0,0 +1,54 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Executes the given [action] until getting of successful result specified number of [times]. | ||||||
|  |  * | ||||||
|  |  * A zero-based index of current iteration is passed as a parameter to [action]. | ||||||
|  |  */ | ||||||
|  | inline fun <R> repeatOnFailure( | ||||||
|  |     onFailure: (Throwable) -> Boolean, | ||||||
|  |     action: () -> R | ||||||
|  | ): Result<R> { | ||||||
|  |     do { | ||||||
|  |         runCatching { | ||||||
|  |             action() | ||||||
|  |         }.onFailure { | ||||||
|  |             if (!onFailure(it)) { | ||||||
|  |                 return Result.failure(it) | ||||||
|  |             } | ||||||
|  |         }.onSuccess { | ||||||
|  |             return Result.success(it) | ||||||
|  |         } | ||||||
|  |     } while (true) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Executes the given [action] until getting of successful result specified number of [times]. | ||||||
|  |  * | ||||||
|  |  * A zero-based index of current iteration is passed as a parameter to [action]. | ||||||
|  |  */ | ||||||
|  | inline fun <R> repeatOnFailure( | ||||||
|  |     times: Int, | ||||||
|  |     onEachFailure: (Throwable) -> Unit = {}, | ||||||
|  |     action: (Int) -> R | ||||||
|  | ): Optional<R> { | ||||||
|  |     var i = 0 | ||||||
|  |     val result = repeatOnFailure( | ||||||
|  |         { | ||||||
|  |             onEachFailure(it) | ||||||
|  |             if (i < times) { | ||||||
|  |                 i++ | ||||||
|  |                 true | ||||||
|  |             } else { | ||||||
|  |                 false | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     ) { | ||||||
|  |         action(i) | ||||||
|  |     } | ||||||
|  |     return if (result.isSuccess) { | ||||||
|  |         Optional.presented(result.getOrThrow()) | ||||||
|  |     } else { | ||||||
|  |         Optional.absent() | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,26 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Will try to execute [action] and, if any exception will happen, execution will be retried. | ||||||
|  |  * This process will happen at most [count] times. There is no any limits on [count] value, but [action] will run at | ||||||
|  |  * least once and [retryOnFailure] will return its result if it is successful | ||||||
|  |  */ | ||||||
|  | inline fun <T> retryOnFailure(count: Int, action: () -> T): T { | ||||||
|  |     var triesCount = 0 | ||||||
|  |     while (true) { | ||||||
|  |         val result = runCatching { | ||||||
|  |             action() | ||||||
|  |         }.onFailure { | ||||||
|  |             triesCount++ | ||||||
|  |  | ||||||
|  |             if (triesCount >= count) { | ||||||
|  |                 throw it | ||||||
|  |             } else { | ||||||
|  |                 null | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (result.isSuccess) return result.getOrThrow() | ||||||
|  |     } | ||||||
|  |     error("Unreachable code: retry must throw latest exception if error happen or success value if not") | ||||||
|  | } | ||||||
| @@ -0,0 +1,6 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | val FixedSignsRange = 0 .. 100 | ||||||
|  |  | ||||||
|  | expect fun Float.fixed(signs: Int): Float | ||||||
|  | expect fun Double.fixed(signs: Int): Double | ||||||
| @@ -0,0 +1,5 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | fun <T> Iterable<T>.withReplacedAt(i: Int, block: (T) -> T): List<T> = take(i) + block(elementAt(i)) + drop(i + 1) | ||||||
|  | fun <T> Iterable<T>.withReplaced(t: T, block: (T) -> T): List<T> = withReplacedAt(indexOf(t), block) | ||||||
|  |  | ||||||
| @@ -11,7 +11,7 @@ class DiffUtilsTests { | |||||||
|         val withIndex = oldList.withIndex() |         val withIndex = oldList.withIndex() | ||||||
|  |  | ||||||
|         for (count in 1 .. (floor(oldList.size.toFloat() / 2).toInt())) { |         for (count in 1 .. (floor(oldList.size.toFloat() / 2).toInt())) { | ||||||
|             for ((i, v) in withIndex) { |             for ((i, _) in withIndex) { | ||||||
|                 if (i + count > oldList.lastIndex) { |                 if (i + count > oldList.lastIndex) { | ||||||
|                     continue |                     continue | ||||||
|                 } |                 } | ||||||
| @@ -32,7 +32,7 @@ class DiffUtilsTests { | |||||||
|         val withIndex = oldList.withIndex() |         val withIndex = oldList.withIndex() | ||||||
|  |  | ||||||
|         for (count in 1 .. (floor(oldList.size.toFloat() / 2).toInt())) { |         for (count in 1 .. (floor(oldList.size.toFloat() / 2).toInt())) { | ||||||
|             for ((i, v) in withIndex) { |             for ((i, _) in withIndex) { | ||||||
|                 if (i + count > oldList.lastIndex) { |                 if (i + count > oldList.lastIndex) { | ||||||
|                     continue |                     continue | ||||||
|                 } |                 } | ||||||
| @@ -54,8 +54,8 @@ class DiffUtilsTests { | |||||||
|         val oldList = (0 until 10).map { it.toString() } |         val oldList = (0 until 10).map { it.toString() } | ||||||
|         val withIndex = oldList.withIndex() |         val withIndex = oldList.withIndex() | ||||||
|  |  | ||||||
|         for (step in 0 until oldList.size) { |         for (step in oldList.indices) { | ||||||
|             for ((i, v) in withIndex) { |             for ((i, _) in withIndex) { | ||||||
|                 val mutable = oldList.toMutableList() |                 val mutable = oldList.toMutableList() | ||||||
|                 val changes = ( |                 val changes = ( | ||||||
|                     if (step == 0) i until oldList.size else (i until oldList.size step step) |                     if (step == 0) i until oldList.size else (i until oldList.size step step) | ||||||
| @@ -73,4 +73,83 @@ class DiffUtilsTests { | |||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     @Test | ||||||
|  |     fun testThatSimpleRemoveApplyWorks() { | ||||||
|  |         val oldList = (0 until 10).toList() | ||||||
|  |         val withIndex = oldList.withIndex() | ||||||
|  |  | ||||||
|  |         for (count in 1 .. (floor(oldList.size.toFloat() / 2).toInt())) { | ||||||
|  |             for ((i, _) in withIndex) { | ||||||
|  |                 if (i + count > oldList.lastIndex) { | ||||||
|  |                     continue | ||||||
|  |                 } | ||||||
|  |                 val removedSublist = oldList.subList(i, i + count) | ||||||
|  |                 val mutableOldList = oldList.toMutableList() | ||||||
|  |                 val targetList = oldList - removedSublist | ||||||
|  |  | ||||||
|  |                 mutableOldList.applyDiff(targetList) | ||||||
|  |  | ||||||
|  |                 assertEquals( | ||||||
|  |                     targetList, | ||||||
|  |                     mutableOldList | ||||||
|  |                 ) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Test | ||||||
|  |     fun testThatSimpleAddApplyWorks() { | ||||||
|  |         val oldList = (0 until 10).map { it.toString() } | ||||||
|  |         val withIndex = oldList.withIndex() | ||||||
|  |  | ||||||
|  |         for (count in 1 .. (floor(oldList.size.toFloat() / 2).toInt())) { | ||||||
|  |             for ((i, _) in withIndex) { | ||||||
|  |                 if (i + count > oldList.lastIndex) { | ||||||
|  |                     continue | ||||||
|  |                 } | ||||||
|  |                 val addedSublist = oldList.subList(i, i + count).map { "added$it" } | ||||||
|  |                 val mutable = oldList.toMutableList() | ||||||
|  |                 mutable.addAll(i, addedSublist) | ||||||
|  |                 val mutableOldList = oldList.toMutableList() | ||||||
|  |  | ||||||
|  |                 mutableOldList.applyDiff(mutable) | ||||||
|  |  | ||||||
|  |                 assertEquals( | ||||||
|  |                     mutable, | ||||||
|  |                     mutableOldList | ||||||
|  |                 ) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Test | ||||||
|  |     fun testThatSimpleChangesApplyWorks() { | ||||||
|  |         val oldList = (0 until 10).map { it.toString() } | ||||||
|  |         val withIndex = oldList.withIndex() | ||||||
|  |  | ||||||
|  |         for (step in oldList.indices) { | ||||||
|  |             for ((i, _) in withIndex) { | ||||||
|  |                 val mutable = oldList.toMutableList() | ||||||
|  |  | ||||||
|  |                 val newList = if (step == 0) { | ||||||
|  |                     i until oldList.size | ||||||
|  |                 } else { | ||||||
|  |                     i until oldList.size step step | ||||||
|  |                 } | ||||||
|  |                 newList.forEach { index -> | ||||||
|  |                     IndexedValue(index, mutable[index]) to IndexedValue(index, "changed$index").also { | ||||||
|  |                         mutable[index] = it.value | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 val mutableOldList = oldList.toMutableList() | ||||||
|  |                 mutableOldList.applyDiff(mutable) | ||||||
|  |                 assertEquals( | ||||||
|  |                     mutable, | ||||||
|  |                     mutableOldList | ||||||
|  |                 ) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -0,0 +1,29 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | import kotlin.test.Test | ||||||
|  | import kotlin.test.assertEquals | ||||||
|  |  | ||||||
|  | class PercentageTests { | ||||||
|  |     @Test | ||||||
|  |     fun testCompareTo() { | ||||||
|  |         val step = 0.01 | ||||||
|  |  | ||||||
|  |         var i = Percentage.START.of1 | ||||||
|  |         while (i <= Percentage.COMPLETED.of1) { | ||||||
|  |             val percentageI = Percentage(i) | ||||||
|  |  | ||||||
|  |             var j = Percentage.START.of1 | ||||||
|  |             while (j <= Percentage.COMPLETED.of1) { | ||||||
|  |                 val percentageJ = Percentage(j) | ||||||
|  |  | ||||||
|  |                 assertEquals(percentageI.of1.compareTo(percentageJ.of1), percentageI.compareTo(percentageJ)) | ||||||
|  |                 assertEquals(percentageI.of1 > percentageJ.of1, percentageI > percentageJ) | ||||||
|  |                 assertEquals(percentageI.of1 < percentageJ.of1, percentageI < percentageJ) | ||||||
|  |  | ||||||
|  |                 j += step | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             i += step | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,21 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | import kotlin.test.Test | ||||||
|  | import kotlin.test.assertEquals | ||||||
|  |  | ||||||
|  | class WithReplacedTest { | ||||||
|  |     @Test | ||||||
|  |     fun testReplaced() { | ||||||
|  |         val data = 0 until 10 | ||||||
|  |         val testData = Int.MAX_VALUE | ||||||
|  |  | ||||||
|  |         for (i in 0 until data.last) { | ||||||
|  |             val withReplaced = data.withReplacedAt(i) { | ||||||
|  |                 testData | ||||||
|  |             } | ||||||
|  |             val dataAsMutableList = data.toMutableList() | ||||||
|  |             dataAsMutableList[i] = testData | ||||||
|  |             assertEquals(withReplaced, dataAsMutableList.toList()) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -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,13 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | import kotlinx.browser.window | ||||||
|  |  | ||||||
|  | fun copyToClipboard(text: String): Boolean { | ||||||
|  |     return runCatching { | ||||||
|  |         window.navigator.clipboard.writeText( | ||||||
|  |             text | ||||||
|  |         ) | ||||||
|  |     }.onFailure { | ||||||
|  |         it.printStackTrace() | ||||||
|  |     }.isSuccess | ||||||
|  | } | ||||||
| @@ -0,0 +1,29 @@ | |||||||
|  | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
|  | import kotlinx.browser.window | ||||||
|  | import org.w3c.files.Blob | ||||||
|  | import org.w3c.files.BlobPropertyBag | ||||||
|  | import kotlin.js.json | ||||||
|  |  | ||||||
|  | external class ClipboardItem(data: dynamic) | ||||||
|  |  | ||||||
|  | inline fun Blob.convertToClipboardItem(): ClipboardItem { | ||||||
|  |     val itemData: dynamic = json(this.type to this) | ||||||
|  |     return ClipboardItem(itemData) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | suspend fun copyImageURLToClipboard(imageUrl: String): Boolean { | ||||||
|  |     return runCatching { | ||||||
|  |         val response = window.fetch(imageUrl).await() | ||||||
|  |         val blob = response.blob().await() | ||||||
|  |         val data = arrayOf( | ||||||
|  |             Blob( | ||||||
|  |                 arrayOf(blob), | ||||||
|  |                 BlobPropertyBag("image/png") | ||||||
|  |             ).convertToClipboardItem() | ||||||
|  |         ).asDynamic() | ||||||
|  |         window.navigator.clipboard.write(data) | ||||||
|  |     }.onFailure { | ||||||
|  |         it.printStackTrace() | ||||||
|  |     }.isSuccess | ||||||
|  | } | ||||||
| @@ -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,58 @@ | |||||||
|  | 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) | ||||||
|  |  | ||||||
|  | actual val MPPFilePathSeparator: Char | ||||||
|  |     get() = '/' | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * @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() | ||||||
|  | } | ||||||
|  |  | ||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user