mirror of
				https://github.com/InsanusMokrassar/MicroUtils.git
				synced 2025-10-29 03:00:31 +00:00 
			
		
		
		
	Compare commits
	
		
			7 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 216c03205c | |||
| ab112aa7a4 | |||
| d85b3d0da9 | |||
| 67b9a03366 | |||
| 3ac56dcfd3 | |||
| d1021d283a | |||
| 97ed973cb5 | 
							
								
								
									
										7
									
								
								.github/workflows/dokka_push.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										7
									
								
								.github/workflows/dokka_push.yml
									
									
									
									
										vendored
									
									
								
							| @@ -10,9 +10,12 @@ jobs: | |||||||
|       - uses: actions/checkout@v2 |       - uses: actions/checkout@v2 | ||||||
|       - uses: actions/setup-java@v1 |       - uses: actions/setup-java@v1 | ||||||
|         with: |         with: | ||||||
|           java-version: 17 |           java-version: 11 | ||||||
|  |       - name: Fix android 32.0.0 dx | ||||||
|  |         continue-on-error: true | ||||||
|  |         run: cd /usr/local/lib/android/sdk/build-tools/32.0.0/ && mv d8 dx && cd lib  && mv d8.jar dx.jar | ||||||
|       - name: Build |       - name: Build | ||||||
|         run: ./gradlew build && ./gradlew dokkaHtml |         run: ./gradlew dokkaHtml | ||||||
|       - name: Publish KDocs |       - name: Publish KDocs | ||||||
|         uses: peaceiris/actions-gh-pages@v3 |         uses: peaceiris/actions-gh-pages@v3 | ||||||
|         with: |         with: | ||||||
|   | |||||||
| @@ -1,14 +1,17 @@ | |||||||
| 
 | 
 | ||||||
| name: Build | name: Publish package to GitHub Packages | ||||||
| on: [push] | on: [push] | ||||||
| jobs: | jobs: | ||||||
|   build: |   publishing: | ||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|     steps: |     steps: | ||||||
|       - uses: actions/checkout@v2 |       - uses: actions/checkout@v2 | ||||||
|       - uses: actions/setup-java@v1 |       - uses: actions/setup-java@v1 | ||||||
|         with: |         with: | ||||||
|           java-version: 17 |           java-version: 11 | ||||||
|  |       - name: Fix android 32.0.0 dx | ||||||
|  |         continue-on-error: true | ||||||
|  |         run: cd /usr/local/lib/android/sdk/build-tools/32.0.0/ && mv d8 dx && cd lib  && mv d8.jar dx.jar | ||||||
|       - name: Rewrite version |       - name: Rewrite version | ||||||
|         run: | |         run: | | ||||||
|           branch="`echo "${{ github.ref }}" | grep -o "[^/]*$"`" |           branch="`echo "${{ github.ref }}" | grep -o "[^/]*$"`" | ||||||
| @@ -19,6 +22,7 @@ jobs: | |||||||
|         run: ./gradlew build |         run: ./gradlew build | ||||||
|       - name: Publish |       - name: Publish | ||||||
|         continue-on-error: true |         continue-on-error: true | ||||||
|         run: ./gradlew publishAllPublicationsToGiteaRepository |         run: ./gradlew --no-parallel publishAllPublicationsToGithubPackagesRepository | ||||||
|         env: |         env: | ||||||
|           GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} |           GITHUBPACKAGES_USER: ${{ github.actor }} | ||||||
|  |           GITHUBPACKAGES_PASSWORD: ${{ secrets.GITHUB_TOKEN }} | ||||||
							
								
								
									
										8
									
								
								.space.kts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								.space.kts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | |||||||
|  | job("Build and run tests") { | ||||||
|  |     container(displayName = "Run gradle build", image = "openjdk:11") { | ||||||
|  |         kotlinScript { api -> | ||||||
|  |             // here can be your complex logic | ||||||
|  |             api.gradlew("build") | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										878
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										878
									
								
								CHANGELOG.md
									
									
									
									
									
								
							| @@ -1,882 +1,6 @@ | |||||||
| # Changelog | # Changelog | ||||||
|  |  | ||||||
| ## 0.20.18 | ## 0.9.25 | ||||||
|  |  | ||||||
| * `Coroutines`: |  | ||||||
|     * `SpecialMutableStateFlow` now extends `MutableStateFlow` |  | ||||||
|     * `Compose`: |  | ||||||
|         * Deprecate `FlowState` due to its complexity in fixes |  | ||||||
|  |  | ||||||
| ## 0.20.17 |  | ||||||
|  |  | ||||||
| * `Versions`: |  | ||||||
|     * `Serialization`: `1.6.1` -> `1.6.2` |  | ||||||
|  |  | ||||||
| ## 0.20.16 |  | ||||||
|  |  | ||||||
| * `Versions`: |  | ||||||
|     * `Exposed`: `0.44.1` -> `0.45.0` |  | ||||||
| * `Coroutines`: |  | ||||||
|     * Add `SpecialMutableStateFlow` |  | ||||||
|     * `Compose`: |  | ||||||
|         * Add `FlowState` |  | ||||||
|  |  | ||||||
| ## 0.20.15 |  | ||||||
|  |  | ||||||
| * `Versions`: |  | ||||||
|     * `Kotlin`: `1.9.20` -> `1.9.21` |  | ||||||
|     * `KSLog`: `1.3.0` -> `1.3.1` |  | ||||||
|     * `Compose`: `1.5.10` -> `1.5.11` |  | ||||||
|  |  | ||||||
| ## 0.20.14 |  | ||||||
|  |  | ||||||
| * `Versions`: |  | ||||||
|     * `Serialization`: `1.6.0` -> `1.6.1` |  | ||||||
|     * `KSLog`: `1.2.4` -> `1.3.0` |  | ||||||
|  |  | ||||||
| ## 0.20.13 |  | ||||||
|  |  | ||||||
| * `Versions`: |  | ||||||
|     * `Ktor`: `2.3.5` -> `2.3.6` |  | ||||||
|     * `UUID`: `0.8.1` -> `0.8.2` |  | ||||||
|  |  | ||||||
| ## 0.20.12 |  | ||||||
|  |  | ||||||
| **It is experimental migration onto new gradle version. Be careful in use of this version** |  | ||||||
|  |  | ||||||
| **This update have JDK 17 in `compatibility` and `target` versions** |  | ||||||
|  |  | ||||||
| ## 0.20.11 |  | ||||||
|  |  | ||||||
| * `Versions`: |  | ||||||
|     * `Kotlin`: `1.9.20-RC2` -> `1.9.20` |  | ||||||
|     * `Exposed`: `0.44.0` -> `0.44.1` |  | ||||||
|     * `Compose`: `1.5.10-rc02` -> `1.5.10` |  | ||||||
| * `Coroutines`: |  | ||||||
|     * `SmartRWLocker` now will wait first unlock of write mutex for acquiring read |  | ||||||
|  |  | ||||||
| ## 0.20.10 |  | ||||||
|  |  | ||||||
| * `Versions`: |  | ||||||
|     * `Kotlin`: `1.9.20-RC` -> `1.9.20-RC1` |  | ||||||
|     * `KSLog`: `1.2.1` -> `1.2.2` |  | ||||||
|     * `Compose`: `1.5.10-rc01` -> `1.5.10-rc02` |  | ||||||
|     * `RecyclerView`: `1.3.1` -> `1.3.2` |  | ||||||
|  |  | ||||||
| ## 0.20.9 |  | ||||||
|  |  | ||||||
| * Most of common modules now supports `linuxArm64` target |  | ||||||
|  |  | ||||||
| ## 0.20.8 |  | ||||||
|  |  | ||||||
| **THIS VERSION CONTAINS UPDATES OF DEPENDENCIES UP TO RC VERSIONS. USE WITH CAUTION** |  | ||||||
|  |  | ||||||
| * `Versions`: |  | ||||||
|     * `Kotlin`: `1.9.20-Beta2` -> `1.9.20-RC` |  | ||||||
|     * `Compose`: `1.5.10-beta02` -> `1.5.10-rc01` |  | ||||||
|  |  | ||||||
| ## 0.20.7 |  | ||||||
|  |  | ||||||
| **THIS VERSION CONTAINS UPDATES OF DEPENDENCIES UP TO BETA VERSIONS. USE WITH CAUTION** |  | ||||||
|  |  | ||||||
| * `Versions`: |  | ||||||
|     * `Kotlin`: `1.9.10` -> `1.9.20-Beta2` |  | ||||||
|     * `Compose`: `1.5.1` -> `1.5.10-beta02` |  | ||||||
|     * `Exposed`: `0.43.0` -> `0.44.0` |  | ||||||
|     * `Ktor`: `2.3.4` -> `2.3.5` |  | ||||||
|     * `Koin`: `3.4.3` -> `3.5.0` |  | ||||||
|     * `Okio`: `3.5.0` -> `3.6.0` |  | ||||||
|     * `Android Core`: `1.10.1` -> `1.12.0` |  | ||||||
|     * `Android Compose Material`: `1.1.1` -> `1.1.2` |  | ||||||
|  |  | ||||||
| ## 0.20.6 |  | ||||||
|  |  | ||||||
| * `Repos`: |  | ||||||
|     * `Exposed` |  | ||||||
|         * Fixes in exposed key-values repos |  | ||||||
|  |  | ||||||
| ## 0.20.5 |  | ||||||
|  |  | ||||||
| * `Coroutines`: |  | ||||||
|     * Fixes in `SmartRWLocker` |  | ||||||
|  |  | ||||||
| ## 0.20.4 |  | ||||||
|  |  | ||||||
| * `Versions`: |  | ||||||
|     * `Kotlin`: `1.9.0` -> `1.9.10` |  | ||||||
|     * `KSLog`: `1.2.0` -> `1.2.1` |  | ||||||
|     * `Compose`: `1.5.0` -> `1.5.1` |  | ||||||
|     * `UUID`: `0.8.0` -> `0.8.1` |  | ||||||
|  |  | ||||||
| ## 0.20.3 |  | ||||||
|  |  | ||||||
| * `Versions`: |  | ||||||
|     * `Compose`: `1.4.3` -> `1.5.0` |  | ||||||
|     * `Exposed`: `0.42.1` -> `0.43.0` |  | ||||||
|     * `Ktor`: `2.3.3` -> `2.3.4` |  | ||||||
| * `Repos`: |  | ||||||
|     * `Cache`: |  | ||||||
|         * Fixes in locks of caches |  | ||||||
|  |  | ||||||
| ## 0.20.2 |  | ||||||
|  |  | ||||||
| * All main repos uses `SmartRWLocker` |  | ||||||
| * `Versions`: |  | ||||||
|     * `Serialization`: `1.5.1` -> `1.6.0` |  | ||||||
|     * `Exposed`: `0.42.0` -> `0.42.1` |  | ||||||
|     * `Korlibs`: `4.0.9` -> `4.0.10` |  | ||||||
| * `Androis SDK`: `33` -> `34` |  | ||||||
|  |  | ||||||
| ## 0.20.1 |  | ||||||
|  |  | ||||||
| * `SmallTextField`: |  | ||||||
|     * Module is initialized |  | ||||||
| * `Pickers`: |  | ||||||
|     * Module is initialized |  | ||||||
| * `Coroutines`: |  | ||||||
|     * Add `SmartSemaphore` |  | ||||||
|     * Add `SmartRWLocker` |  | ||||||
|  |  | ||||||
| ## 0.20.0 |  | ||||||
|  |  | ||||||
| * `Versions`: |  | ||||||
|     * `Kotlin`: `1.8.22` -> `1.9.0` |  | ||||||
|     * `KSLog`: `1.1.1` -> `1.2.0` |  | ||||||
|     * `Exposed`: `0.41.1` -> `0.42.0` |  | ||||||
|     * `UUID`: `0.7.1` -> `0.8.0` |  | ||||||
|     * `Korlibs`: `4.0.3` -> `4.0.9` |  | ||||||
|     * `Ktor`: `2.3.2` -> `2.3.3` |  | ||||||
|     * `Okio`: `3.4.0` -> `3.5.0` |  | ||||||
|  |  | ||||||
| ## 0.19.9 |  | ||||||
|  |  | ||||||
| * `Versions`: |  | ||||||
|     * `Koin`: `3.4.2` -> `3.4.3` |  | ||||||
| * `Startup`: |  | ||||||
|     * Now it is possible to start application in synchronous way |  | ||||||
|  |  | ||||||
| ## 0.19.8 |  | ||||||
|  |  | ||||||
| * `Versions`: |  | ||||||
|     * `Coroutines`: `1.7.2` -> `1.7.3` |  | ||||||
|     * `Kotlin`: `1.8.20` -> `1.8.22` |  | ||||||
|     * `Compose`: `1.4.1` -> `1.4.3` |  | ||||||
|     * `Okio`: `3.3.0` -> `3.4.0` |  | ||||||
|     * `RecyclerView`: `1.3.0` -> `1.3.1` |  | ||||||
|     * `Fragment`: `1.6.0` -> `1.6.1` |  | ||||||
| * `Repos`: |  | ||||||
|     * Fixes In `KeyValueRepo.clear()` of almost all inheritors of `KeyValueRepo` |  | ||||||
|     * `Cache`: |  | ||||||
|         * All full caches got `skipStartInvalidate` property. By default, this property is `false` and fully caching repos |  | ||||||
|           will be automatically invalidated on start of their work |  | ||||||
|  |  | ||||||
| ## 0.19.7 |  | ||||||
|  |  | ||||||
| * `Versions`: |  | ||||||
|     * `Coroutines`: `1.7.1` -> `1.7.2` |  | ||||||
|  |  | ||||||
| ## 0.19.6 |  | ||||||
|  |  | ||||||
| * `Versions`: |  | ||||||
|     * `Coroutines`: `1.6.4` -> `1.7.1` |  | ||||||
|     * `Ktor`: `2.3.1` -> `2.3.2` |  | ||||||
|     * `Compose`: `1.4.0` -> `1.4.1` |  | ||||||
|  |  | ||||||
| ## 0.19.5 |  | ||||||
|  |  | ||||||
| * `Repos`: |  | ||||||
|     * `Generator`: |  | ||||||
|         * Fixes in new type generation |  | ||||||
|  |  | ||||||
| ## 0.19.4 |  | ||||||
|  |  | ||||||
| * `Versions`: |  | ||||||
|     * `Koin`: `3.4.1` -> `3.4.2` |  | ||||||
|     * `Android Fragments`: `1.5.7` -> `1.6.0` |  | ||||||
| * `Koin` |  | ||||||
|     * `Generator` |  | ||||||
|         * Fixes in new generic generator part |  | ||||||
|  |  | ||||||
| ## 0.19.3 |  | ||||||
|  |  | ||||||
| * `Koin` |  | ||||||
|     * `Generator` |  | ||||||
|         * New getter methods now available with opportunity to use parameters |  | ||||||
|         * Old notation `*Single` and `*Factory` is deprecated since this release. With old |  | ||||||
|           will be generated new `single*` and `factory*` notations for new generations |  | ||||||
|         * Add opportunity to use generic-oriented koin definitions |  | ||||||
|  |  | ||||||
| ## 0.19.2 |  | ||||||
|  |  | ||||||
| * `Versions`: |  | ||||||
|     * `Ktor`: `2.3.0` -> `2.3.1` |  | ||||||
|     * `Koin`: `3.4.0` -> `3.4.1` |  | ||||||
|     * `Uuid`: `0.7.0` -> `0.7.1` |  | ||||||
|  |  | ||||||
| ## 0.19.1 |  | ||||||
|  |  | ||||||
| * `Versions`: |  | ||||||
|     * `Korlibs`: `4.0.1` -> `4.0.3` |  | ||||||
|     * `Kotlin Poet`: `1.13.2` -> `1.14.0` |  | ||||||
|  |  | ||||||
| ## 0.19.0 |  | ||||||
|  |  | ||||||
| * `Versions`: |  | ||||||
|     * `Korlibs`: `3.4.0` -> `4.0.1` |  | ||||||
|  |  | ||||||
| ## 0.18.4 |  | ||||||
|  |  | ||||||
| * `Koin`: |  | ||||||
|     * New extension `lazyInject` |  | ||||||
|  |  | ||||||
| ## 0.18.3 |  | ||||||
|  |  | ||||||
| * `Versions`: |  | ||||||
|     * `Serialization`: `1.5.0` -> `1.5.1` |  | ||||||
|     * `Android Cor Ktx`: `1.10.0` -> `1.10.1` |  | ||||||
|  |  | ||||||
| ## 0.18.2 |  | ||||||
|  |  | ||||||
| * `Startup`: |  | ||||||
|     * Now internal `Json` is fully customizable |  | ||||||
|  |  | ||||||
| ## 0.18.1 |  | ||||||
|  |  | ||||||
| * `Common`: |  | ||||||
|     * Add `MapDiff` |  | ||||||
| * `Coroutines`: |  | ||||||
|     * Add `SmartMutex` |  | ||||||
|  |  | ||||||
| ## 0.18.0 |  | ||||||
|  |  | ||||||
| **ALL PREVIOUSLY DEPRECATED FUNCTIONALITY HAVE BEEN REMOVED** |  | ||||||
|  |  | ||||||
| * `Versions`: |  | ||||||
|     * `Android Fragments`: `1.5.6` -> `1.5.7` |  | ||||||
| * `Ktor`: |  | ||||||
|     * `Server`: |  | ||||||
|         * Now it is possible to take query parameters as list |  | ||||||
| * `Repos`: |  | ||||||
|     * `Common`: |  | ||||||
|         * New `WriteKeyValuesRepo.removeWithValue` |  | ||||||
|     * `Cache`: |  | ||||||
|         * Rename full caching factories functions to `fullyCached` |  | ||||||
|  |  | ||||||
| ## 0.17.8 |  | ||||||
|  |  | ||||||
| * `Versions`: |  | ||||||
|     * `Ktor`: `2.2.4` -> `2.3.0` |  | ||||||
|  |  | ||||||
| ## 0.17.7 |  | ||||||
|  |  | ||||||
| * `Versions`: |  | ||||||
|     * `Android CoreKtx`: `1.9.0` -> `1.10.0` |  | ||||||
| * `Startup`: |  | ||||||
|     * Add support of `linuxX64` and `mingwX64` platforms |  | ||||||
|  |  | ||||||
| ## 0.17.6 |  | ||||||
|  |  | ||||||
| * `Versions`: |  | ||||||
|     * `Kotlin`: `1.8.10` -> `1.8.20` |  | ||||||
|     * `KSLog`: `1.0.0` -> `1.1.1` |  | ||||||
|     * `Compose`: `1.3.1` -> `1.4.0` |  | ||||||
|     * `Koin`: `3.3.2` -> `3.4.0` |  | ||||||
|     * `RecyclerView`: `1.2.1` -> `1.3.0` |  | ||||||
|     * `Fragment`: `1.5.5` -> `1.5.6` |  | ||||||
| * Experimentally (`!!!`) add `linuxX64` and `mingwX64` targets |  | ||||||
|  |  | ||||||
| ## 0.17.5 |  | ||||||
|  |  | ||||||
| * `Common`: |  | ||||||
|     * Conversations of number primitives with bounds care |  | ||||||
| * `Repos`: |  | ||||||
|     * `Common`: |  | ||||||
|         * By default, `getAll` for repos will take all the size of repo as page size |  | ||||||
|         * New extension for all built-in repos `maxPagePagination` |  | ||||||
|     * All the repos got `getAll` functions |  | ||||||
|  |  | ||||||
| ## 0.17.4 |  | ||||||
|  |  | ||||||
| * `Serialization`: |  | ||||||
|     * `Mapper`: |  | ||||||
|         * Module inited |  | ||||||
| * `Versions`: |  | ||||||
|     * `Compose`: `1.3.1-rc02` -> `1.3.1` |  | ||||||
|  |  | ||||||
| ## 0.17.3 |  | ||||||
|  |  | ||||||
| * `Common`: |  | ||||||
|     * Add `fixed` extensions for `Float` and `Double` |  | ||||||
|     * New function `emptyDiff` |  | ||||||
|     * Now you may pass custom `comparisonFun` to all `diff` functions |  | ||||||
|  |  | ||||||
| ## 0.17.2 |  | ||||||
|  |  | ||||||
| * `FSM`: |  | ||||||
|     * `DefaultStatesManager.onUpdateContextsConflictResolver` and `DefaultStatesManager.onStartContextsConflictResolver` now return `false` by default |  | ||||||
|  |  | ||||||
| ## 0.17.1 |  | ||||||
|  |  | ||||||
| * **Hotfix** for absence of jvm dependencies in android modules |  | ||||||
| * `Versions`: |  | ||||||
|     * `Ktor`: `2.2.3` -> `2.2.4` |  | ||||||
|  |  | ||||||
| ## 0.17.0 |  | ||||||
|  |  | ||||||
| * `Versions`: |  | ||||||
|     * `Kotlin`: `1.7.20` -> `1.8.10` |  | ||||||
|     * `Serialization`: `1.4.1` -> `1.5.0` |  | ||||||
|     * `KSLog`: `0.5.4` -> `1.0.0` |  | ||||||
|     * `AppCompat`: `1.6.0` -> `1.6.1` |  | ||||||
|  |  | ||||||
| ## 0.16.13 |  | ||||||
|  |  | ||||||
| * `Repos`: |  | ||||||
|     * `Generator`: |  | ||||||
|         * Module has been created |  | ||||||
|  |  | ||||||
| ## 0.16.12 |  | ||||||
|  |  | ||||||
| * `Repos`: |  | ||||||
|     * `Exposed`: |  | ||||||
|         * `CommonExposedRepo.selectByIds` uses `foldRight` by default instead of raw foreach |  | ||||||
| * `Koin`: |  | ||||||
|     * `Generator`: |  | ||||||
|         * Module has been created |  | ||||||
|  |  | ||||||
| ## 0.16.11 |  | ||||||
|  |  | ||||||
| * `LanguageCodes`: |  | ||||||
|     * In android and JVM targets now available `toJavaLocale` and from Java `Locale` conversations from/to |  | ||||||
|       `IetfLanguageCode` |  | ||||||
|  |  | ||||||
| ## 0.16.10 |  | ||||||
|  |  | ||||||
| * `Repos`: |  | ||||||
|     * `Cache`: |  | ||||||
|         * New transformer type: `ReadCRUDFromKeyValueRepo` |  | ||||||
|         * New transformer type: `ReadKeyValueFromCRUDRepo` |  | ||||||
| * `Pagination`: |  | ||||||
|     * New `paginate` extensions with `reversed` support for `List`/`Set` |  | ||||||
|  |  | ||||||
| ## 0.16.9 |  | ||||||
|  |  | ||||||
| * `Versions`: |  | ||||||
|     * `Koin`: `3.2.2` -> `3.3.2` |  | ||||||
|     * `AppCompat`: `1.5.1` -> `1.6.0` |  | ||||||
| * `Ktor`: |  | ||||||
|     * `Client` |  | ||||||
|         * `HttpResponse.bodyOrNull` now retrieve callback to check if body should be received or null |  | ||||||
|         * New extension `HttpResponse.bodyOrNullOnNoContent` |  | ||||||
|  |  | ||||||
| ## 0.16.8 |  | ||||||
|  |  | ||||||
| * `Versions`: |  | ||||||
|     * `Ktor`: `2.2.2` -> `2.2.3` |  | ||||||
| * `Ktor`: |  | ||||||
|     * `Client` |  | ||||||
|         * Fixes in `HttpClient.uniUpload` |  | ||||||
|     * `Server` |  | ||||||
|         * Fixes in `PartData.FileItem.download` |  | ||||||
| * `Repos`: |  | ||||||
|     * `Cache`: |  | ||||||
|         * New type of caches: `FallbackCacheRepo` |  | ||||||
|         * Fixes in `Write*` variants of cached repos |  | ||||||
|         * New type `ActionWrapper` |  | ||||||
|         * New `AutoRecache*` classes for all types of repos as `FallbackCacheRepo`s |  | ||||||
|     * `Common`: |  | ||||||
|         * New transformations for key-value and key-values vice-verse |  | ||||||
|         * Fixes in `FileReadKeyValueRepo` |  | ||||||
|  |  | ||||||
| ## 0.16.7 |  | ||||||
|  |  | ||||||
| * `Common`: |  | ||||||
|     * New extensions `ifTrue`/`ifFalse`/`alsoIfTrue`/`alsoIfFalse`/`letIfTrue`/`letIfFalse` |  | ||||||
|     * `Diff` now is serializable |  | ||||||
|     * Add `IndexedValue` serializer |  | ||||||
|     * `repeatOnFailure` extending: now you may pass any lambda to check if continue to try/do something |  | ||||||
|     * `Compose`: |  | ||||||
|         * New extension `MutableState.asState` |  | ||||||
| * `Coroutines`: |  | ||||||
|     * `Compose`: |  | ||||||
|         * All the `Flow` conversations to compose `State`/`MutableState`/`SnapshotStateList`/`List` got several new |  | ||||||
|         parameters |  | ||||||
|         * `Flow.toMutableState` now is deprecated in favor to `asMutableComposeState` |  | ||||||
| * `Repos`: |  | ||||||
|     * `Cache`: |  | ||||||
|         * New type `FullCacheRepo` |  | ||||||
|         * New type `CommonCacheRepo` |  | ||||||
|         * `CacheRepo` got `invalidate` method. It will fully reload `FullCacheRepo` and just clear `CommonCacheRepo` |  | ||||||
|         * New extensions `KVCache.actualizeAll` |  | ||||||
|  |  | ||||||
| ## 0.16.6 |  | ||||||
|  |  | ||||||
| * `Startup`: |  | ||||||
|     * `Launcher`: |  | ||||||
|         * Improvements in `StartLauncherPlugin#start` methods |  | ||||||
|         * Add opportunity to pass second argument on `JVM` platform as log level |  | ||||||
| * `Repos`: |  | ||||||
|     * `Ktor`: |  | ||||||
|         * `Client`: |  | ||||||
|             * All clients repos got opportunity to customize their flows |  | ||||||
|     * `Exposed`: |  | ||||||
|         * Extensions `eqOrIsNull` and `neqOrIsNotNull` for `Column` |  | ||||||
|  |  | ||||||
| ## 0.16.5 |  | ||||||
|  |  | ||||||
| * `Versions`: |  | ||||||
|     * `Ktor`: `2.2.1` -> `2.2.2` |  | ||||||
|  |  | ||||||
| ## 0.16.4 |  | ||||||
|  |  | ||||||
| * `Coroutines`: |  | ||||||
|     * Create `launchInCurrentThread` |  | ||||||
|  |  | ||||||
| ## 0.16.3 |  | ||||||
|  |  | ||||||
| * `Startup`: |  | ||||||
|     * `Launcher`: |  | ||||||
|         * All starting API have been moved into `StartLauncherPlugin` and do not require serialize/deserialize cycle for now |  | ||||||
|  |  | ||||||
| ## 0.16.2 |  | ||||||
|  |  | ||||||
| * `Versions`: |  | ||||||
|     * `Compose`: `1.2.1` -> `1.2.2` |  | ||||||
| * `Startup`: |  | ||||||
|     * Module become available on `JS` target |  | ||||||
|  |  | ||||||
| ## 0.16.1 |  | ||||||
|  |  | ||||||
| * `Coroutines`: |  | ||||||
|     * New `runCatchingSafely`/`safelyWithResult` with receivers |  | ||||||
| * `SafeWrapper`: |  | ||||||
|     * Module inited |  | ||||||
|  |  | ||||||
| ## 0.16.0 |  | ||||||
|  |  | ||||||
| * `Versions`: |  | ||||||
|     * `Ktor`: `2.1.3` -> `2.2.1` |  | ||||||
|     * `Android Fragment`: `1.5.3` -> `1.5.5` |  | ||||||
|  |  | ||||||
| ## 0.15.1 |  | ||||||
|  |  | ||||||
| * `Startup`: |  | ||||||
|     * Inited :) |  | ||||||
|     * `Plugin`: |  | ||||||
|         * Inited :) |  | ||||||
|     * `Launcher`: |  | ||||||
|         * Inited :) |  | ||||||
|  |  | ||||||
| ## 0.15.0 |  | ||||||
|  |  | ||||||
| * `Repos`: |  | ||||||
|     * `CRUD`: |  | ||||||
|         * `Common`: |  | ||||||
|             * New method `ReadCRUDRepo#getIdsByPagination` |  | ||||||
|         * `Android`: |  | ||||||
|             * `AbstractAndroidCRUDRepo` got new abstract method `toId` |  | ||||||
|         * `Exposed`: |  | ||||||
|             * `CommonExposedRepo` new abstract property `asId` |  | ||||||
|         * `Ktor`: |  | ||||||
|             * `Client`: |  | ||||||
|                 * `KtorReadCRUDRepoClient` now requires `paginationIdType` |  | ||||||
| * `LanguageCodes`: |  | ||||||
|     * Updates and fixes in generation |  | ||||||
| * `MimeTypes`: |  | ||||||
|     * Updates and fixes in generation |  | ||||||
|  |  | ||||||
| ## 0.14.4 |  | ||||||
|  |  | ||||||
| * `Common`: |  | ||||||
|     * `JVM`: |  | ||||||
|         * New extension `downloadToTempFile` |  | ||||||
| * `Ktor`: |  | ||||||
|     * `Server`: |  | ||||||
|         * Small fix in `handleUniUpload` |  | ||||||
|         * `ApplicationCall#uniloadMultipartFile` now uses `uniloadMultipart` |  | ||||||
|     * `Common`: |  | ||||||
|         * New extension `downloadToTempFile` |  | ||||||
|     * `Client`: |  | ||||||
|         * New extensions on top of `uniUpload` |  | ||||||
|  |  | ||||||
| ## 0.14.3 |  | ||||||
|  |  | ||||||
| * `Common`: |  | ||||||
|     * New type `Progress` |  | ||||||
| * `Ktor`: |  | ||||||
|     * `Client`: |  | ||||||
|         * New universal `uniUpload` extension for `HttpClient` |  | ||||||
|     * `Server`: |  | ||||||
|         * New universal `handleUniUpload` extension for `ApplicationCall` |  | ||||||
|         * Add extensions `download` and `downloadToTemporalFile` |  | ||||||
|  |  | ||||||
| ## 0.14.2 |  | ||||||
|  |  | ||||||
| * `Versions`: |  | ||||||
|     * `Exposed`: `0.40.1` -> `0.41.1` |  | ||||||
|  |  | ||||||
|  |  | ||||||
| ## 0.14.1 |  | ||||||
|  |  | ||||||
| * `Versions`: |  | ||||||
|     * `Klock`: `3.3.1` -> `3.4.0` |  | ||||||
|     * `UUID`: `0.5.0` -> `0.6.0` |  | ||||||
|  |  | ||||||
| ## 0.14.0 |  | ||||||
|  |  | ||||||
| **ALL DEPRECATIONS HAVE BEEN REMOVED** |  | ||||||
|  |  | ||||||
| * `Versions`: |  | ||||||
|     * `Kotlin`: `1.7.10` -> `1.7.20` |  | ||||||
|     * `Klock`: `3.3.0` -> `3.3.1` |  | ||||||
|     * `Compose`: `1.2.0` -> `1.2.1` |  | ||||||
|     * `Exposed`: `0.39.2` -> `0.40.1` |  | ||||||
|  |  | ||||||
| ## 0.13.2 |  | ||||||
|  |  | ||||||
| * `Versions`: |  | ||||||
|     * `Klock`: `3.1.0` -> `3.3.0` |  | ||||||
|     * `Ktor`: `2.1.2` -> `2.1.3` |  | ||||||
|  |  | ||||||
| ## 0.13.1 |  | ||||||
|  |  | ||||||
| * `Repos`: |  | ||||||
|     * `Exposed`: |  | ||||||
|       * `AbstractExposedWriteCRUDRepo#createAndInsertId` now is optional and returns nullable value |  | ||||||
|  |  | ||||||
| ## 0.13.0 |  | ||||||
|  |  | ||||||
| **ALL DEPRECATIONS HAVE BEEN REMOVED** |  | ||||||
| **A LOT OF KTOR METHODS RELATED TO UnifierRouter/UnifiedRequester HAVE BEEN REMOVED** |  | ||||||
|  |  | ||||||
| * `Repos`: |  | ||||||
|   * `Exposed`: |  | ||||||
|     * `AbstractExposedWriteCRUDRepo` got two new methods: `update` with `it` as `UpdateBuilder<Int>` and `createAndInsertId` |  | ||||||
|       * Old `update` method has been deprecated and not recommended to override anymore in realizations |  | ||||||
|       * Old `insert` method now is `open` instead of `abstract` and can be omitted |  | ||||||
|     * `AbstractExposedKeyValueRepo` got two new methods: `update` with `it` as `UpdateBuilder<Int>` and `insertKey` |  | ||||||
|       * Old `update` method has been deprecated and not recommended to override anymore |  | ||||||
|       * Old `insert` method now is `open` instead of `abstract` and can be omitted in realizations |  | ||||||
|  |  | ||||||
| ## 0.12.17 |  | ||||||
|  |  | ||||||
| * `Versions`: |  | ||||||
|     * `JB Compose`: `1.2.0-alpha01-dev774` -> `1.2.0-beta02` |  | ||||||
|     * `Ktor`: `2.1.1` -> `2.1.2` |  | ||||||
|     * `Koin`: `3.2.0` -> `3.2.2` |  | ||||||
|  |  | ||||||
| ## 0.12.16 |  | ||||||
|  |  | ||||||
| * `Coroutines`: |  | ||||||
|   * `Android`: |  | ||||||
|     * Add class `FlowOnHierarchyChangeListener` |  | ||||||
|     * Add `ViewGroup#setOnHierarchyChangeListenerRecursively(OnHierarchyChangeListener)` |  | ||||||
|  |  | ||||||
| ## 0.12.15 |  | ||||||
|  |  | ||||||
| * `Common`: |  | ||||||
|     * `applyDiff` will return `Diff` object since this release |  | ||||||
|     * `Android`: |  | ||||||
|       * New functions/extensions `findViewsByTag` and `findViewsByTagInActivity` |  | ||||||
| * `Coroutines`: |  | ||||||
|     * Add `Flow` extensions `flatMap`, `flatMapNotNull` and `flatten` |  | ||||||
|     * Add `Flow` extensions `takeNotNull` and `filterNotNull` |  | ||||||
|  |  | ||||||
| ## 0.12.14 |  | ||||||
|  |  | ||||||
| * `Versions`: |  | ||||||
|   * `Android CoreKTX`: `1.8.0` -> `1.9.0` |  | ||||||
|   * `Android AppCompat`: `1.4.2` -> `1.5.1` |  | ||||||
|   * Android Compile SDK: 32 -> 33 |  | ||||||
|   * Android Build Tools: 32.0.0 -> 33.0.0 |  | ||||||
| * `Common`: |  | ||||||
|   * `Android`: |  | ||||||
|     * Add `argumentOrNull`/`argumentOrThrow` delegates for fragments |  | ||||||
| * `Coroutines`: |  | ||||||
|   * Rewrite `awaitFirstWithDeferred` onto `CompletableDeferred` instead of coroutines suspending |  | ||||||
|  |  | ||||||
| ## 0.12.13 |  | ||||||
|  |  | ||||||
| * `Coroutines`: |  | ||||||
|   * Add opportunity to use markers in actors (solution of [#160](https://github.com/InsanusMokrassar/MicroUtils/issues/160)) |  | ||||||
| * `Koin`: |  | ||||||
|   * Module inited :) |  | ||||||
| * `Repos`: |  | ||||||
|   * `Android`: |  | ||||||
|     * Add typealias `KeyValueSPRepo` and opportunity to create shared preferences `KeyValue` repo with `KeyValueStore(...)` (fix of [#155](https://github.com/InsanusMokrassar/MicroUtils/issues/155)) |  | ||||||
|  |  | ||||||
| ## 0.12.12 |  | ||||||
|  |  | ||||||
| * `Common`: |  | ||||||
|   * `Compose`: |  | ||||||
|     * `JS`: |  | ||||||
|       * Add `SkeletonAnimation` stylesheet |  | ||||||
|  |  | ||||||
| ## 0.12.11 |  | ||||||
|  |  | ||||||
| * `Repos`: |  | ||||||
|     * `Cache`: |  | ||||||
|         * Override `KeyValue` cache method `values` |  | ||||||
|  |  | ||||||
| ## 0.12.10 |  | ||||||
|  |  | ||||||
| * `Repos`: |  | ||||||
|     * `Cache`: |  | ||||||
|         * Hotfix in key values `get` |  | ||||||
|  |  | ||||||
| ## 0.12.9 |  | ||||||
|  |  | ||||||
| * `Versions`: |  | ||||||
|   * `Klock`: `3.0.0` -> `3.1.0` |  | ||||||
| * `Repos`: |  | ||||||
|     * `Cache`: |  | ||||||
|       * Fixes in key values cache |  | ||||||
|  |  | ||||||
| ## 0.12.8 |  | ||||||
|  |  | ||||||
| * `Versions`: |  | ||||||
|     * `Ktor`: `2.1.0` -> `2.1.1` |  | ||||||
|     * `Compose`: `1.2.0-alpha01-dev764` -> `1.2.0-alpha01-dev774` |  | ||||||
| * `Ktor`: |  | ||||||
|   * `Client`: |  | ||||||
|     * New extension `HttpClient#bodyOrNull` which returns `null` in case when server responded with `No Content` (204) |  | ||||||
|   * `Server`: |  | ||||||
|     * New extension `ApplicationCall#respondOrNoContent` which responds `No Content` (204) when passed data is null |  | ||||||
|  |  | ||||||
| ## 0.12.7 |  | ||||||
|  |  | ||||||
| * `Repos`: |  | ||||||
|   * `Cache`: |  | ||||||
|     * Force `WriteCRUDCacheRepo` to subscribe on new and updated objects of parent repo |  | ||||||
| * `Pagination`: |  | ||||||
|   * New function `changeResultsUnchecked(Pagination)` |  | ||||||
|  |  | ||||||
| ## 0.12.6 |  | ||||||
|  |  | ||||||
| * `MimeeTypes>`: |  | ||||||
|   * Fixed absence of `image/*` in known mime types |  | ||||||
|  |  | ||||||
| ## 0.12.5 |  | ||||||
|  |  | ||||||
| * `Repos`: |  | ||||||
|   * `Exposed`: |  | ||||||
|     * Fixes in `paginate` extensions |  | ||||||
|  |  | ||||||
| ## 0.12.4 |  | ||||||
|  |  | ||||||
| * `Versions`: |  | ||||||
|     * `Kotlin`: `1.7.0` -> `1.7.10` |  | ||||||
|     * `Compose`: `1.2.0-alpha01-dev755` -> `1.2.0-alpha01-dev764` |  | ||||||
|  |  | ||||||
| ## 0.12.3 |  | ||||||
|  |  | ||||||
| * `Repos`: |  | ||||||
|   * `Exposed`: |  | ||||||
|     * Add abstract exposed variants of `KeyValue` and `KeyValues` repos |  | ||||||
|     * Add new extension `Query#selectPaginated` |  | ||||||
|  |  | ||||||
| ## 0.12.2 |  | ||||||
|  |  | ||||||
| * `Versions`: |  | ||||||
|   * `Serialization`: `1.4.0-RC` -> `1.4.0` |  | ||||||
|   * `Compose`: `1.2.0-alpha01-dev753` -> `1.2.0-alpha01-dev755` |  | ||||||
|  |  | ||||||
| ## 0.12.1 |  | ||||||
|  |  | ||||||
| * `Versions`: |  | ||||||
|   * `Ktor`: `2.0.3` -> `2.1.0` |  | ||||||
|  |  | ||||||
| ## 0.12.0 |  | ||||||
|  |  | ||||||
| **OLD DEPRECATIONS HAVE BEEN REMOVED** |  | ||||||
|  |  | ||||||
| **MINIMAL ANDROID API HAS BEEN ENLARGED UP TO API 21 (Android 5.0)** |  | ||||||
|  |  | ||||||
| * `Versions` |  | ||||||
|   * `Kotlin`: `1.6.21` -> `1.7.0` |  | ||||||
|   * `Coroutines`: `1.6.3` -> `1.6.4` |  | ||||||
|   * `Exposed`: `0.38.2` -> `0.39.2` |  | ||||||
|   * `Compose`: `1.2.0-alpha01-dev729` -> `1.2.0-alpha01-dev753` |  | ||||||
|   * `Klock`: `2.7.0` -> `3.0.0` |  | ||||||
|   * `uuid`: `0.4.1` -> `0.5.0` |  | ||||||
|   * `Android Core KTX`: `1.7.0` -> `1.8.0` |  | ||||||
|   * `Android AppCompat`: `1.4.1` -> `1.4.2` |  | ||||||
| * `Ktor`: |  | ||||||
|   * All previously standard functions related to work with binary data by default have been deprecated |  | ||||||
|  |  | ||||||
| ## 0.11.14 |  | ||||||
|  |  | ||||||
| * `Pagination`: |  | ||||||
|     * `PaginationResult` got new field `objectsNumber` which by default is a times between `pagesNumber` and `size` |  | ||||||
|  |  | ||||||
| ## 0.11.13 |  | ||||||
|  |  | ||||||
| * `Versions`: |  | ||||||
|     * `Coroutines`: `1.6.3` -> `1.6.4` |  | ||||||
|     * `Compose`: `1.2.0-alpha01-dev629` -> `1.2.0-alpha01-dev731` |  | ||||||
|  |  | ||||||
| ## 0.11.12 |  | ||||||
|  |  | ||||||
| * `Repos`: |  | ||||||
|   * `Common`: |  | ||||||
|     * `JVM`: |  | ||||||
|       * Fixes in `ReadFileKeyValueRepo` methods (`values`/`keys`) |  | ||||||
|  |  | ||||||
| ## 0.11.11 |  | ||||||
|  |  | ||||||
| * `Crypto`: |  | ||||||
|   * `hmacSha256` has been deprecated |  | ||||||
| * `Ktor`: |  | ||||||
|   * `Client`: |  | ||||||
|     * `BodyPair` has been deprecated |  | ||||||
| * `Repos`: |  | ||||||
|     * `Cache`: |  | ||||||
|       * New interface `CacheRepo` |  | ||||||
|       * New interface `FullCacheRepo` |  | ||||||
|       * `actualize*` methods inside of full cache repos now open for overriding |  | ||||||
|  |  | ||||||
| ## 0.11.10 |  | ||||||
|  |  | ||||||
| * `Repos`: |  | ||||||
|   * `Cache`: |  | ||||||
|     * `KVCache` has been replaced to the package `dev.inmo.micro_utils.repos.cache` |  | ||||||
|     * `SimpleKVCache` has been replaced to the package `dev.inmo.micro_utils.repos.cache` |  | ||||||
|     * New `KVCache` subtype - `FullKVCache` |  | ||||||
|     * Add `Full*` variants of standard repos |  | ||||||
|     * Add `cached`/`caching` (for write repos) extensions for all standard types of repos |  | ||||||
|  |  | ||||||
| ## 0.11.9 |  | ||||||
|  |  | ||||||
| * `Versions` |  | ||||||
|     * `Coroutines`: `1.6.1` -> `1.6.3` |  | ||||||
|     * `Ktor`: `2.0.2` -> `2.0.3` |  | ||||||
|     * `Compose`: `1.2.0-alpha01-dev686` -> `1.2.0-alpha01-dev729` |  | ||||||
|  |  | ||||||
| ## 0.11.8 |  | ||||||
|  |  | ||||||
| * `Repos`: |  | ||||||
|     * `Common`: |  | ||||||
|         * Fixes in `FileKeyValueRepo` |  | ||||||
|  |  | ||||||
| ## 0.11.7 |  | ||||||
|  |  | ||||||
| * `Common`: |  | ||||||
|   * New abstractions `SimpleMapper` and `SimpleSuspendableMapper` |  | ||||||
| * `Repos`: |  | ||||||
|   * `Common`: |  | ||||||
|     * Add mappers for `CRUDRepo` |  | ||||||
|  |  | ||||||
| ## 0.11.6 |  | ||||||
|  |  | ||||||
| * `FSM`: |  | ||||||
|   * `Common` |  | ||||||
|     * Several fixes related to the jobs handling |  | ||||||
|  |  | ||||||
| ## 0.11.5 |  | ||||||
|  |  | ||||||
| * `Coroutines`: |  | ||||||
|   * `Compose`: |  | ||||||
|     * Add extension `StateFlow#asMutableComposeListState` and `StateFlow#asComposeList` |  | ||||||
|     * Add extension `StateFlow#asMutableComposeState`/`StateFlow#asComposeState` |  | ||||||
|  |  | ||||||
| ## 0.11.4 |  | ||||||
|  |  | ||||||
| **THIS VERSION HAS BEEN BROKEN, DO NOT USE IT** |  | ||||||
|  |  | ||||||
| ## 0.11.3 |  | ||||||
|  |  | ||||||
| * `Ktor`: |  | ||||||
|     * Support of `WebSockets` has been improved |  | ||||||
|       * `Client`: |  | ||||||
|         * New extensions: `HttpClient#openBaseWebSocketFlow`, `HttpClient#openWebSocketFlow`, `HttpClient#openSecureWebSocketFlow` |  | ||||||
|  |  | ||||||
| ## 0.11.2 |  | ||||||
|  |  | ||||||
| * `Ktor`: |  | ||||||
|   * Support of `WebSockets` has been improved and added fixes inside of clients |  | ||||||
|  |  | ||||||
| ## 0.11.1 |  | ||||||
|  |  | ||||||
| * `Repos` |  | ||||||
|   * `Ktor` |  | ||||||
|     * In `configureReadKeyValueRepoRoutes` and `configureReadKeyValuesRepoRoutes` configurators fixed requiring of `reversed` property |  | ||||||
|  |  | ||||||
| ## 0.11.0 |  | ||||||
|  |  | ||||||
| * `Versions` |  | ||||||
|     * `UUID`: `0.4.0` -> `0.4.1` |  | ||||||
| * `Ktor` |  | ||||||
|   * `Client`: |  | ||||||
|     * New extension fun `HttpResponse#throwOnUnsuccess` |  | ||||||
|     * All old functions, classes and extensions has been rewritten with new ktor-way with types info and keeping `ContentNegotiation` in mind |  | ||||||
|   * `Server`: |  | ||||||
|       * All old functions, classes and extensions has been rewritten with new ktor-way with types info and keeping `ContentNegotiation` in mind |  | ||||||
| * `Repos` |  | ||||||
|   * `Ktor`: |  | ||||||
|     * Fully rewritten work with all declared repositories |  | ||||||
|     * All old functions, classes and extensions has been rewritten with new ktor-way with types info and keeping `ContentNegotiation` in mind |  | ||||||
|  |  | ||||||
| ## 0.10.8 |  | ||||||
|  |  | ||||||
| * `Common` |  | ||||||
|     * Add `Element.isOverflow*` extension properties |  | ||||||
|  |  | ||||||
| ## 0.10.7 |  | ||||||
|  |  | ||||||
| * `Pagination`: |  | ||||||
|     * Now it is possible to use `doForAll*` and `getForAll` functions in non suspend places |  | ||||||
|  |  | ||||||
| ## 0.10.6 |  | ||||||
|  |  | ||||||
| * `Versions` |  | ||||||
|     * `Ktor`: `2.0.1` -> `2.0.2` |  | ||||||
| * `Common` |  | ||||||
|     * `JS`: |  | ||||||
|         * Add `ResizeObserver` functionality |  | ||||||
|  |  | ||||||
| ## 0.10.5 |  | ||||||
|  |  | ||||||
| * `Versions` |  | ||||||
|     * `Compose`: `1.2.0-alpha01-dev683` -> `1.2.0-alpha01-dev686` |  | ||||||
| * `Repos` |  | ||||||
|     * `Android`: |  | ||||||
|         * New function `SharedPreferencesKeyValueRepo` |  | ||||||
| * `FSM` |  | ||||||
|     * Add `StateHandlingErrorHandler` and opportunity to handle states handling errors |  | ||||||
|  |  | ||||||
| ## 0.10.4 |  | ||||||
|  |  | ||||||
| * `Versions`: |  | ||||||
|     * `Serialization`: `1.3.2` -> `1.3.3` |  | ||||||
|  |  | ||||||
| ## 0.10.3 |  | ||||||
|  |  | ||||||
| * `Versions`: |  | ||||||
|     * `Compose`: `1.2.0-alpha01-dev682` -> `1.2.0-alpha01-dev683` |  | ||||||
| * `Coroutines`: |  | ||||||
|     * Fixes in `AccumulatorFlow` |  | ||||||
|  |  | ||||||
| ## 0.10.2 |  | ||||||
|  |  | ||||||
| * `Versions`: |  | ||||||
|     * `Compose`: `1.2.0-alpha01-dev675` -> `1.2.0-alpha01-dev682` |  | ||||||
|  |  | ||||||
| ## 0.10.1 |  | ||||||
|  |  | ||||||
| * `Versions`: |  | ||||||
|     * `Ktor`: `2.0.0` -> `2.0.1` |  | ||||||
| * `Crypto`: |  | ||||||
|     * Add `hmacSha256` |  | ||||||
|     * Add `hex` |  | ||||||
|  |  | ||||||
| ## 0.10.0 |  | ||||||
|  |  | ||||||
| * `Versions`: |  | ||||||
|     * `Kotlin`: `1.6.10` -> `1.6.21` |  | ||||||
|     * `Compose`: `1.1.1` -> `1.2.0-alpha01-dev675` |  | ||||||
|     * `Exposed`: `0.37.3` -> `0.38.2` |  | ||||||
|     * `Ktor`: `1.6.8` -> `2.0.0` |  | ||||||
|     * `Dokka`: `1.6.10` -> `1.6.21` |  | ||||||
|  |  | ||||||
| ## 0.9.24 | ## 0.9.24 | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1 +0,0 @@ | |||||||
| <manifest/> |  | ||||||
							
								
								
									
										1
									
								
								android/alerts/common/src/main/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								android/alerts/common/src/main/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | <manifest package="dev.inmo.micro_utils.android.alerts.common"/> | ||||||
| @@ -1 +0,0 @@ | |||||||
| <manifest/> |  | ||||||
							
								
								
									
										1
									
								
								android/alerts/recyclerview/src/main/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								android/alerts/recyclerview/src/main/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | <manifest package="dev.inmo.micro_utils.android.alerts.recyclerview"/> | ||||||
| @@ -1,18 +0,0 @@ | |||||||
| plugins { |  | ||||||
|     id "org.jetbrains.kotlin.multiplatform" |  | ||||||
|     id "org.jetbrains.kotlin.plugin.serialization" |  | ||||||
|     id "com.android.library" |  | ||||||
|     alias(libs.plugins.jb.compose) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| apply from: "$mppProjectWithSerializationAndComposePresetPath" |  | ||||||
|  |  | ||||||
| kotlin { |  | ||||||
|     sourceSets { |  | ||||||
|         androidMain { |  | ||||||
|             dependencies { |  | ||||||
|                 api project(":micro_utils.android.smalltextfield") |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1 +0,0 @@ | |||||||
| <manifest/> |  | ||||||
| @@ -1,27 +0,0 @@ | |||||||
| 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, |  | ||||||
|         ) |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,222 +0,0 @@ | |||||||
| package dev.inmo.micro_utils.android.pickers |  | ||||||
|  |  | ||||||
| import androidx.compose.animation.core.Animatable |  | ||||||
| import androidx.compose.animation.core.exponentialDecay |  | ||||||
| import androidx.compose.foundation.clickable |  | ||||||
| import androidx.compose.foundation.gestures.* |  | ||||||
| import androidx.compose.foundation.layout.* |  | ||||||
| import androidx.compose.foundation.text.KeyboardActions |  | ||||||
| import androidx.compose.foundation.text.KeyboardOptions |  | ||||||
| import androidx.compose.material.ContentAlpha |  | ||||||
| import androidx.compose.material.IconButton |  | ||||||
| import androidx.compose.material.ProvideTextStyle |  | ||||||
| import androidx.compose.material.icons.Icons |  | ||||||
| import androidx.compose.material.icons.filled.KeyboardArrowDown |  | ||||||
| import androidx.compose.material.icons.filled.KeyboardArrowUp |  | ||||||
| import androidx.compose.material3.* |  | ||||||
| import androidx.compose.runtime.* |  | ||||||
| import androidx.compose.ui.Alignment |  | ||||||
| import androidx.compose.ui.ExperimentalComposeUiApi |  | ||||||
| import androidx.compose.ui.Modifier |  | ||||||
| import androidx.compose.ui.draw.alpha |  | ||||||
| import androidx.compose.ui.focus.FocusRequester |  | ||||||
| import androidx.compose.ui.focus.focusRequester |  | ||||||
| import androidx.compose.ui.geometry.Offset |  | ||||||
| import androidx.compose.ui.graphics.Color |  | ||||||
| import androidx.compose.ui.input.pointer.PointerInputScope |  | ||||||
| import androidx.compose.ui.input.pointer.pointerInput |  | ||||||
| import androidx.compose.ui.platform.LocalDensity |  | ||||||
| import androidx.compose.ui.text.ExperimentalTextApi |  | ||||||
| import androidx.compose.ui.text.TextStyle |  | ||||||
| import androidx.compose.ui.text.input.KeyboardType |  | ||||||
| import androidx.compose.ui.unit.IntOffset |  | ||||||
| import androidx.compose.ui.unit.center |  | ||||||
| import androidx.compose.ui.unit.dp |  | ||||||
| import dev.inmo.micro_utils.android.smalltextfield.SmallTextField |  | ||||||
| import kotlinx.coroutines.launch |  | ||||||
| import kotlin.math.abs |  | ||||||
| import kotlin.math.absoluteValue |  | ||||||
| import kotlin.math.roundToInt |  | ||||||
|  |  | ||||||
| private inline fun PointerInputScope.checkContains(offset: Offset): Boolean { |  | ||||||
|     return ((size.center.x - offset.x).absoluteValue < size.width / 2) && ((size.center.y - offset.y).absoluteValue < size.height / 2) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // src: https://gist.github.com/vganin/a9a84653a9f48a2d669910fbd48e32d5 |  | ||||||
|  |  | ||||||
| @OptIn(ExperimentalTextApi::class, ExperimentalComposeUiApi::class) |  | ||||||
| @Composable |  | ||||||
| fun NumberPicker( |  | ||||||
|     number: Int, |  | ||||||
|     modifier: Modifier = Modifier, |  | ||||||
|     range: IntRange? = null, |  | ||||||
|     textStyle: TextStyle = LocalTextStyle.current, |  | ||||||
|     arrowsColor: Color = MaterialTheme.colorScheme.primary, |  | ||||||
|     allowUseManualInput: Boolean = true, |  | ||||||
|     onStateChanged: (Int) -> Unit = {}, |  | ||||||
| ) { |  | ||||||
|     val coroutineScope = rememberCoroutineScope() |  | ||||||
|     val numbersColumnHeight = 36.dp |  | ||||||
|     val halvedNumbersColumnHeight = numbersColumnHeight / 2 |  | ||||||
|     val halvedNumbersColumnHeightPx = with(LocalDensity.current) { halvedNumbersColumnHeight.toPx() } |  | ||||||
|  |  | ||||||
|     fun animatedStateValue(offset: Float): Int = number - (offset / halvedNumbersColumnHeightPx).toInt() |  | ||||||
|  |  | ||||||
|     val animatedOffset = remember { Animatable(0f) }.apply { |  | ||||||
|         if (range != null) { |  | ||||||
|             val offsetRange = remember(number, range) { |  | ||||||
|                 val value = number |  | ||||||
|                 val first = -(range.last - value) * halvedNumbersColumnHeightPx |  | ||||||
|                 val last = -(range.first - value) * halvedNumbersColumnHeightPx |  | ||||||
|                 first..last |  | ||||||
|             } |  | ||||||
|             updateBounds(offsetRange.start, offsetRange.endInclusive) |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|     val coercedAnimatedOffset = animatedOffset.value % halvedNumbersColumnHeightPx |  | ||||||
|     val animatedStateValue = animatedStateValue(animatedOffset.value) |  | ||||||
|     val disabledArrowsColor = arrowsColor.copy(alpha = ContentAlpha.disabled) |  | ||||||
|  |  | ||||||
|     val inputFieldShown = if (allowUseManualInput) { |  | ||||||
|         remember { mutableStateOf(false) } |  | ||||||
|     } else { |  | ||||||
|         null |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     Column( |  | ||||||
|         modifier = modifier |  | ||||||
|             .wrapContentSize() |  | ||||||
|             .draggable( |  | ||||||
|                 orientation = Orientation.Vertical, |  | ||||||
|                 state = rememberDraggableState { deltaY -> |  | ||||||
|                     if (inputFieldShown ?.value != true) { |  | ||||||
|                         coroutineScope.launch { |  | ||||||
|                             animatedOffset.snapTo(animatedOffset.value + deltaY) |  | ||||||
|                         } |  | ||||||
|                     } |  | ||||||
|                 }, |  | ||||||
|                 onDragStopped = { velocity -> |  | ||||||
|                     if (inputFieldShown ?.value != true) { |  | ||||||
|                         coroutineScope.launch { |  | ||||||
|                             val endValue = animatedOffset.fling( |  | ||||||
|                                 initialVelocity = velocity, |  | ||||||
|                                 animationSpec = exponentialDecay(frictionMultiplier = 20f), |  | ||||||
|                                 adjustTarget = { target -> |  | ||||||
|                                     val coercedTarget = target % halvedNumbersColumnHeightPx |  | ||||||
|                                     val coercedAnchors = |  | ||||||
|                                         listOf(-halvedNumbersColumnHeightPx, 0f, halvedNumbersColumnHeightPx) |  | ||||||
|                                     val coercedPoint = coercedAnchors.minByOrNull { abs(it - coercedTarget) }!! |  | ||||||
|                                     val base = |  | ||||||
|                                         halvedNumbersColumnHeightPx * (target / halvedNumbersColumnHeightPx).toInt() |  | ||||||
|                                     coercedPoint + base |  | ||||||
|                                 } |  | ||||||
|                             ).endState.value |  | ||||||
|  |  | ||||||
|                             onStateChanged(animatedStateValue(endValue)) |  | ||||||
|                             animatedOffset.snapTo(0f) |  | ||||||
|                         } |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|             ), |  | ||||||
|         horizontalAlignment = Alignment.CenterHorizontally |  | ||||||
|     ) { |  | ||||||
|         val spacing = 4.dp |  | ||||||
|  |  | ||||||
|         val upEnabled = range == null || range.first < number |  | ||||||
|         IconButton( |  | ||||||
|             { |  | ||||||
|                 onStateChanged(number - 1) |  | ||||||
|                 inputFieldShown ?.value = false |  | ||||||
|             }, |  | ||||||
|             enabled = upEnabled |  | ||||||
|         ) { |  | ||||||
|             Icon(Icons.Default.KeyboardArrowUp, "", tint = if (upEnabled) arrowsColor else disabledArrowsColor) |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         Spacer(modifier = Modifier.height(spacing)) |  | ||||||
|         Box( |  | ||||||
|             modifier = Modifier |  | ||||||
|                 .offset { IntOffset(x = 0, y = coercedAnimatedOffset.roundToInt()) }, |  | ||||||
|             contentAlignment = Alignment.Center |  | ||||||
|         ) { |  | ||||||
|             val baseLabelModifier = Modifier.align(Alignment.Center) |  | ||||||
|             ProvideTextStyle(textStyle) { |  | ||||||
|                 Text( |  | ||||||
|                     text = (animatedStateValue - 1).toString(), |  | ||||||
|                     modifier = baseLabelModifier |  | ||||||
|                         .offset(y = -halvedNumbersColumnHeight) |  | ||||||
|                         .alpha(coercedAnimatedOffset / halvedNumbersColumnHeightPx) |  | ||||||
|                 ) |  | ||||||
|  |  | ||||||
|                 if (inputFieldShown ?.value == true) { |  | ||||||
|                     val currentValue = remember { mutableStateOf(number.toString()) } |  | ||||||
|  |  | ||||||
|                     val focusRequester = remember { FocusRequester() } |  | ||||||
|                     SmallTextField( |  | ||||||
|                         currentValue.value, |  | ||||||
|                         { |  | ||||||
|                             val asDigit = it.toIntOrNull() |  | ||||||
|                             when { |  | ||||||
|                                 (asDigit == null && it.isEmpty()) -> currentValue.value = (range ?.first ?: 0).toString() |  | ||||||
|                                 (asDigit != null && (range == null || asDigit in range)) -> currentValue.value = it |  | ||||||
|                                 else -> { /* do nothing */ } |  | ||||||
|                             } |  | ||||||
|                         }, |  | ||||||
|                         baseLabelModifier.focusRequester(focusRequester).width(IntrinsicSize.Min).pointerInput(number) { |  | ||||||
|                             detectTapGestures { |  | ||||||
|                                 if (!checkContains(it)) { |  | ||||||
|                                     currentValue.value.toIntOrNull() ?.let(onStateChanged) |  | ||||||
|                                     inputFieldShown.value = false |  | ||||||
|                                 } |  | ||||||
|                             } |  | ||||||
|                         }, |  | ||||||
|                         keyboardOptions = KeyboardOptions( |  | ||||||
|                             keyboardType = KeyboardType.Number |  | ||||||
|                         ), |  | ||||||
|                         keyboardActions = KeyboardActions { |  | ||||||
|                             currentValue.value.toIntOrNull() ?.let(onStateChanged) |  | ||||||
|                             inputFieldShown.value = false |  | ||||||
|                         }, |  | ||||||
|                         singleLine = true, |  | ||||||
|                         textStyle = textStyle |  | ||||||
|                     ) |  | ||||||
|                     LaunchedEffect(Unit) { |  | ||||||
|                         focusRequester.requestFocus() |  | ||||||
|                     } |  | ||||||
|                 } else { |  | ||||||
|                     Text( |  | ||||||
|                         text = animatedStateValue.toString(), |  | ||||||
|                         modifier = baseLabelModifier |  | ||||||
|                             .alpha(1 - abs(coercedAnimatedOffset) / halvedNumbersColumnHeightPx) |  | ||||||
|                             .clickable { |  | ||||||
|                                 if (inputFieldShown ?.value == false) { |  | ||||||
|                                     inputFieldShown.value = true |  | ||||||
|                                 } |  | ||||||
|                             } |  | ||||||
|                     ) |  | ||||||
|                 } |  | ||||||
|                 Text( |  | ||||||
|                     text = (animatedStateValue + 1).toString(), |  | ||||||
|                     modifier = baseLabelModifier |  | ||||||
|                         .offset(y = halvedNumbersColumnHeight) |  | ||||||
|                         .alpha(-coercedAnimatedOffset / halvedNumbersColumnHeightPx) |  | ||||||
|                 ) |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|  |  | ||||||
|         Spacer(modifier = Modifier.height(spacing)) |  | ||||||
|  |  | ||||||
|         val downEnabled = range == null || range.last > number |  | ||||||
|         IconButton( |  | ||||||
|             { |  | ||||||
|                 onStateChanged(number + 1) |  | ||||||
|                 inputFieldShown ?.value = false |  | ||||||
|             }, |  | ||||||
|             enabled = downEnabled |  | ||||||
|         ) { |  | ||||||
|             Icon(Icons.Default.KeyboardArrowDown, "", tint = if (downEnabled) arrowsColor else disabledArrowsColor) |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| @@ -1,156 +0,0 @@ | |||||||
| package dev.inmo.micro_utils.android.pickers |  | ||||||
|  |  | ||||||
| import androidx.compose.animation.core.Animatable |  | ||||||
| import androidx.compose.animation.core.exponentialDecay |  | ||||||
| import androidx.compose.foundation.gestures.* |  | ||||||
| import androidx.compose.foundation.layout.* |  | ||||||
| import androidx.compose.foundation.rememberScrollState |  | ||||||
| import androidx.compose.material.ContentAlpha |  | ||||||
| import androidx.compose.material.icons.Icons |  | ||||||
| import androidx.compose.material.icons.filled.KeyboardArrowDown |  | ||||||
| import androidx.compose.material.icons.filled.KeyboardArrowUp |  | ||||||
| import androidx.compose.material3.* |  | ||||||
| import androidx.compose.runtime.* |  | ||||||
| import androidx.compose.ui.Alignment |  | ||||||
| import androidx.compose.ui.ExperimentalComposeUiApi |  | ||||||
| import androidx.compose.ui.Modifier |  | ||||||
| import androidx.compose.ui.draw.alpha |  | ||||||
| import androidx.compose.ui.graphics.Color |  | ||||||
| import androidx.compose.ui.platform.LocalDensity |  | ||||||
| import androidx.compose.ui.text.ExperimentalTextApi |  | ||||||
| import androidx.compose.ui.text.TextStyle |  | ||||||
| import androidx.compose.ui.unit.dp |  | ||||||
| import kotlinx.coroutines.launch |  | ||||||
| import kotlin.math.* |  | ||||||
|  |  | ||||||
| @OptIn(ExperimentalTextApi::class, ExperimentalComposeUiApi::class) |  | ||||||
| @Composable |  | ||||||
| fun <T> SetPicker( |  | ||||||
|     current: T, |  | ||||||
|     dataList: List<T>, |  | ||||||
|     modifier: Modifier = Modifier, |  | ||||||
|     textStyle: TextStyle = LocalTextStyle.current, |  | ||||||
|     arrowsColor: Color = MaterialTheme.colorScheme.primary, |  | ||||||
|     dataToString: @Composable (T) -> String = { it.toString() }, |  | ||||||
|     onStateChanged: (T) -> Unit = {}, |  | ||||||
| ) { |  | ||||||
|     val coroutineScope = rememberCoroutineScope() |  | ||||||
|     val numbersColumnHeight = 8.dp + with(LocalDensity.current) { |  | ||||||
|         textStyle.lineHeight.toDp() |  | ||||||
|     } |  | ||||||
|     val numbersColumnHeightPx = with(LocalDensity.current) { numbersColumnHeight.toPx() } |  | ||||||
|     val halvedNumbersColumnHeight = numbersColumnHeight / 2 |  | ||||||
|     val halvedNumbersColumnHeightPx = with(LocalDensity.current) { halvedNumbersColumnHeight.toPx() } |  | ||||||
|  |  | ||||||
|     val index = dataList.indexOfFirst { it === current }.takeIf { it > -1 } ?: dataList.indexOf(current) |  | ||||||
|     val lastIndex = dataList.size - 1 |  | ||||||
|  |  | ||||||
|     fun animatedStateValue(offset: Float): Int = index - (offset / halvedNumbersColumnHeightPx).toInt() |  | ||||||
|  |  | ||||||
|     val animatedOffset = remember { Animatable(0f) }.apply { |  | ||||||
|         val offsetRange = remember(index, lastIndex) { |  | ||||||
|             val value = index |  | ||||||
|             val first = -(lastIndex - value) * halvedNumbersColumnHeightPx |  | ||||||
|             val last = value * halvedNumbersColumnHeightPx |  | ||||||
|             first..last |  | ||||||
|         } |  | ||||||
|         updateBounds(offsetRange.start, offsetRange.endInclusive) |  | ||||||
|     } |  | ||||||
|     val indexAnimatedOffset = if (animatedOffset.value > 0) { |  | ||||||
|         (index - floor(animatedOffset.value / halvedNumbersColumnHeightPx).toInt()) |  | ||||||
|     } else { |  | ||||||
|         (index - ceil(animatedOffset.value / halvedNumbersColumnHeightPx).toInt()) |  | ||||||
|     } |  | ||||||
|     val coercedAnimatedOffset = animatedOffset.value % halvedNumbersColumnHeightPx |  | ||||||
|     val boxOffset = (indexAnimatedOffset * halvedNumbersColumnHeightPx) - coercedAnimatedOffset |  | ||||||
|     val disabledArrowsColor = arrowsColor.copy(alpha = ContentAlpha.disabled) |  | ||||||
|     val scrollState = rememberScrollState() |  | ||||||
|  |  | ||||||
|     Column( |  | ||||||
|         modifier = modifier |  | ||||||
|             .wrapContentSize() |  | ||||||
|             .draggable( |  | ||||||
|                 orientation = Orientation.Vertical, |  | ||||||
|                 state = rememberDraggableState { deltaY -> |  | ||||||
|                     coroutineScope.launch { |  | ||||||
|                         animatedOffset.snapTo(animatedOffset.value + deltaY) |  | ||||||
|                     } |  | ||||||
|                 }, |  | ||||||
|                 onDragStopped = { velocity -> |  | ||||||
|                     coroutineScope.launch { |  | ||||||
|                         val endValue = animatedOffset.fling( |  | ||||||
|                             initialVelocity = velocity, |  | ||||||
|                             animationSpec = exponentialDecay(frictionMultiplier = 20f), |  | ||||||
|                             adjustTarget = { target -> |  | ||||||
|                                 val coercedTarget = target % halvedNumbersColumnHeightPx |  | ||||||
|                                 val coercedAnchors = |  | ||||||
|                                     listOf(-halvedNumbersColumnHeightPx, 0f, halvedNumbersColumnHeightPx) |  | ||||||
|                                 val coercedPoint = coercedAnchors.minByOrNull { abs(it - coercedTarget) }!! |  | ||||||
|                                 val base = |  | ||||||
|                                     halvedNumbersColumnHeightPx * (target / halvedNumbersColumnHeightPx).toInt() |  | ||||||
|                                 coercedPoint + base |  | ||||||
|                             } |  | ||||||
|                         ).endState.value |  | ||||||
|  |  | ||||||
|                         onStateChanged(dataList.elementAt(animatedStateValue(endValue))) |  | ||||||
|                         animatedOffset.snapTo(0f) |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|             ), |  | ||||||
|         horizontalAlignment = Alignment.CenterHorizontally |  | ||||||
|     ) { |  | ||||||
|         val spacing = 4.dp |  | ||||||
|  |  | ||||||
|         val upEnabled = index > 0 |  | ||||||
|         IconButton( |  | ||||||
|             { |  | ||||||
|                 onStateChanged(dataList.elementAt(index - 1)) |  | ||||||
|             }, |  | ||||||
|             enabled = upEnabled |  | ||||||
|         ) { |  | ||||||
|             Icon(Icons.Default.KeyboardArrowUp, "", tint = if (upEnabled) arrowsColor else disabledArrowsColor) |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         Spacer(modifier = Modifier.height(spacing)) |  | ||||||
|         Box( |  | ||||||
|             modifier = Modifier, |  | ||||||
|             contentAlignment = Alignment.Center |  | ||||||
|         ) { |  | ||||||
|             ProvideTextStyle(textStyle) { |  | ||||||
|                 dataList.forEachIndexed { i, t -> |  | ||||||
|                     val alpha = when { |  | ||||||
|                         i == indexAnimatedOffset - 1 -> coercedAnimatedOffset / halvedNumbersColumnHeightPx |  | ||||||
|                         i == indexAnimatedOffset -> 1 - (abs(coercedAnimatedOffset) / halvedNumbersColumnHeightPx) |  | ||||||
|                         i == indexAnimatedOffset + 1 -> -coercedAnimatedOffset / halvedNumbersColumnHeightPx |  | ||||||
|                         else -> return@forEachIndexed |  | ||||||
|                     } |  | ||||||
|                     val offset = when { |  | ||||||
|                         i == indexAnimatedOffset - 1 && coercedAnimatedOffset > 0 -> coercedAnimatedOffset - halvedNumbersColumnHeightPx |  | ||||||
|                         i == indexAnimatedOffset -> coercedAnimatedOffset |  | ||||||
|                         i == indexAnimatedOffset + 1 && coercedAnimatedOffset < 0 -> coercedAnimatedOffset + halvedNumbersColumnHeightPx |  | ||||||
|                         else -> return@forEachIndexed |  | ||||||
|                     } |  | ||||||
|                     Text( |  | ||||||
|                         text = dataToString(t), |  | ||||||
|                         modifier = Modifier |  | ||||||
|                             .alpha(alpha) |  | ||||||
|                             .offset(y = with(LocalDensity.current) { offset.toDp() }) |  | ||||||
|                     ) |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|  |  | ||||||
|         Spacer(modifier = Modifier.height(spacing)) |  | ||||||
|  |  | ||||||
|         val downEnabled = index < lastIndex |  | ||||||
|         IconButton( |  | ||||||
|             { |  | ||||||
|                 onStateChanged(dataList.elementAt(index + 1)) |  | ||||||
|             }, |  | ||||||
|             enabled = downEnabled |  | ||||||
|         ) { |  | ||||||
|             Icon(Icons.Default.KeyboardArrowDown, "", tint = if (downEnabled) arrowsColor else disabledArrowsColor) |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1 +0,0 @@ | |||||||
| <manifest/> |  | ||||||
							
								
								
									
										1
									
								
								android/recyclerview/src/main/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								android/recyclerview/src/main/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | <manifest package="dev.inmo.micro_utils.android.recyclerview"/> | ||||||
| @@ -1,18 +0,0 @@ | |||||||
| plugins { |  | ||||||
|     id "org.jetbrains.kotlin.multiplatform" |  | ||||||
|     id "org.jetbrains.kotlin.plugin.serialization" |  | ||||||
|     id "com.android.library" |  | ||||||
|     alias(libs.plugins.jb.compose) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| apply from: "$mppProjectWithSerializationAndComposePresetPath" |  | ||||||
|  |  | ||||||
| kotlin { |  | ||||||
|     sourceSets { |  | ||||||
|         androidMain { |  | ||||||
|             dependencies { |  | ||||||
|                 api libs.android.compose.material3 |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1 +0,0 @@ | |||||||
| <manifest/> |  | ||||||
| @@ -1,66 +0,0 @@ | |||||||
| package dev.inmo.micro_utils.android.smalltextfield |  | ||||||
|  |  | ||||||
| import androidx.compose.foundation.interaction.MutableInteractionSource |  | ||||||
| import androidx.compose.foundation.layout.Box |  | ||||||
| import androidx.compose.foundation.layout.Row |  | ||||||
| import androidx.compose.foundation.layout.defaultMinSize |  | ||||||
| import androidx.compose.foundation.text.BasicTextField |  | ||||||
| import androidx.compose.foundation.text.KeyboardActions |  | ||||||
| import androidx.compose.foundation.text.KeyboardOptions |  | ||||||
| import androidx.compose.foundation.text.selection.LocalTextSelectionColors |  | ||||||
| import androidx.compose.material3.* |  | ||||||
| import androidx.compose.runtime.Composable |  | ||||||
| import androidx.compose.runtime.CompositionLocalProvider |  | ||||||
| import androidx.compose.runtime.remember |  | ||||||
| import androidx.compose.ui.Alignment |  | ||||||
| import androidx.compose.ui.Modifier |  | ||||||
| import androidx.compose.ui.graphics.Color |  | ||||||
| import androidx.compose.ui.graphics.Shape |  | ||||||
| import androidx.compose.ui.graphics.SolidColor |  | ||||||
| import androidx.compose.ui.graphics.takeOrElse |  | ||||||
| import androidx.compose.ui.text.TextStyle |  | ||||||
| import androidx.compose.ui.text.input.VisualTransformation |  | ||||||
|  |  | ||||||
| @OptIn(ExperimentalMaterial3Api::class) |  | ||||||
| @Composable |  | ||||||
| fun SmallTextField( |  | ||||||
|     value: String, |  | ||||||
|     onValueChange: (String) -> Unit, |  | ||||||
|     modifier: Modifier = Modifier, |  | ||||||
|     enabled: Boolean = true, |  | ||||||
|     readOnly: Boolean = false, |  | ||||||
|     textStyle: TextStyle = LocalTextStyle.current, |  | ||||||
|     textColor: Color = textStyle.color.takeOrElse { |  | ||||||
|         LocalContentColor.current |  | ||||||
|     }, |  | ||||||
|     visualTransformation: VisualTransformation = VisualTransformation.None, |  | ||||||
|     keyboardOptions: KeyboardOptions = KeyboardOptions.Default, |  | ||||||
|     keyboardActions: KeyboardActions = KeyboardActions.Default, |  | ||||||
|     singleLine: Boolean = false, |  | ||||||
|     maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE, |  | ||||||
|     minLines: Int = 1, |  | ||||||
|     interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, |  | ||||||
| ) { |  | ||||||
|     BasicTextField( |  | ||||||
|         value = value, |  | ||||||
|         modifier = modifier, |  | ||||||
|         onValueChange = onValueChange, |  | ||||||
|         enabled = enabled, |  | ||||||
|         readOnly = readOnly, |  | ||||||
|         textStyle = textStyle.copy( |  | ||||||
|             color = textColor |  | ||||||
|         ), |  | ||||||
|         visualTransformation = visualTransformation, |  | ||||||
|         keyboardOptions = keyboardOptions, |  | ||||||
|         keyboardActions = keyboardActions, |  | ||||||
|         interactionSource = interactionSource, |  | ||||||
|         singleLine = singleLine, |  | ||||||
|         maxLines = maxLines, |  | ||||||
|         minLines = minLines, |  | ||||||
|         cursorBrush = SolidColor( |  | ||||||
|             textStyle.color.takeOrElse { |  | ||||||
|                 LocalContentColor.current |  | ||||||
|             } |  | ||||||
|         ) |  | ||||||
|     ) |  | ||||||
| } |  | ||||||
| @@ -9,7 +9,6 @@ buildscript { | |||||||
|     dependencies { |     dependencies { | ||||||
|         classpath libs.buildscript.kt.gradle |         classpath libs.buildscript.kt.gradle | ||||||
|         classpath libs.buildscript.kt.serialization |         classpath libs.buildscript.kt.serialization | ||||||
|         classpath libs.buildscript.kt.ksp |  | ||||||
|         classpath libs.buildscript.jb.dokka |         classpath libs.buildscript.jb.dokka | ||||||
|         classpath libs.buildscript.gh.release |         classpath libs.buildscript.gh.release | ||||||
|         classpath libs.buildscript.android.gradle |         classpath libs.buildscript.android.gradle | ||||||
| @@ -17,17 +16,11 @@ buildscript { | |||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| plugins { |  | ||||||
|     alias(libs.plugins.versions) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| allprojects { | allprojects { | ||||||
|     repositories { |     repositories { | ||||||
|         mavenLocal() |         mavenLocal() | ||||||
|         mavenCentral() |         mavenCentral() | ||||||
|         google() |         google() | ||||||
|         maven { url "https://maven.pkg.jetbrains.space/public/p/compose/dev" } |  | ||||||
|         maven { url "https://git.inmo.dev/api/packages/InsanusMokrassar/maven" } |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     // temporal crutch until legacy tests will be stabled or legacy target will be removed |     // temporal crutch until legacy tests will be stabled or legacy target will be removed | ||||||
| @@ -42,4 +35,3 @@ allprojects { | |||||||
|  |  | ||||||
| apply from: "./extensions.gradle" | apply from: "./extensions.gradle" | ||||||
| apply from: "./github_release.gradle" | apply from: "./github_release.gradle" | ||||||
| apply from: "./versions_plugin_setup.gradle" |  | ||||||
|   | |||||||
| @@ -4,7 +4,7 @@ plugins { | |||||||
|     id "com.android.library" |     id "com.android.library" | ||||||
| } | } | ||||||
|  |  | ||||||
| apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64ProjectPresetPath" | apply from: "$mppProjectWithSerializationPresetPath" | ||||||
|  |  | ||||||
| kotlin { | kotlin { | ||||||
|     sourceSets { |     sourceSets { | ||||||
| @@ -16,24 +16,6 @@ kotlin { | |||||||
|         androidMain { |         androidMain { | ||||||
|             dependencies { |             dependencies { | ||||||
|                 api project(":micro_utils.coroutines") |                 api project(":micro_utils.coroutines") | ||||||
|                 api libs.android.fragment |  | ||||||
|             } |  | ||||||
|             dependsOn jvmMain |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         linuxX64Main { |  | ||||||
|             dependencies { |  | ||||||
|                 api libs.okio |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|         mingwX64Main { |  | ||||||
|             dependencies { |  | ||||||
|                 api libs.okio |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|         linuxArm64Main { |  | ||||||
|             dependencies { |  | ||||||
|                 api libs.okio |  | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -1 +0,0 @@ | |||||||
| <manifest/> |  | ||||||
| @@ -1,10 +0,0 @@ | |||||||
| 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 } |  | ||||||
| @@ -1,43 +0,0 @@ | |||||||
| package dev.inmo.micro_utils.common.compose |  | ||||||
|  |  | ||||||
| import org.jetbrains.compose.web.css.* |  | ||||||
|  |  | ||||||
| object SkeletonAnimation : StyleSheet() { |  | ||||||
|     val skeletonKeyFrames: CSSNamedKeyframes by keyframes { |  | ||||||
|         to { backgroundPosition("-20% 0") } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     fun CSSBuilder.includeSkeletonStyle( |  | ||||||
|         duration: CSSSizeValue<out CSSUnitTime> = 2.s, |  | ||||||
|         timingFunction: AnimationTimingFunction = AnimationTimingFunction.EaseInOut, |  | ||||||
|         iterationCount: Int? = null, |  | ||||||
|         direction: AnimationDirection = AnimationDirection.Normal, |  | ||||||
|         keyFrames: CSSNamedKeyframes = skeletonKeyFrames, |  | ||||||
|         hideChildren: Boolean = true, |  | ||||||
|         hideText: Boolean = hideChildren |  | ||||||
|     ) { |  | ||||||
|         backgroundImage("linear-gradient(110deg, rgb(236, 236, 236) 40%, rgb(245, 245, 245) 50%, rgb(236, 236, 236) 65%)") |  | ||||||
|         backgroundSize("200% 100%") |  | ||||||
|         backgroundPosition("180% 0") |  | ||||||
|         animation(keyFrames) { |  | ||||||
|             duration(duration) |  | ||||||
|             timingFunction(timingFunction) |  | ||||||
|             iterationCount(iterationCount) |  | ||||||
|             direction(direction) |  | ||||||
|         } |  | ||||||
|         if (hideText) { |  | ||||||
|             property("color", "${Color.transparent} !important") |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         if (hideChildren) { |  | ||||||
|             child(self, universal) style { |  | ||||||
|                 property("visibility", "hidden") |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     val skeleton by style { |  | ||||||
|         includeSkeletonStyle() |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
							
								
								
									
										1
									
								
								common/compose/src/main/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								common/compose/src/main/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | <manifest package="dev.inmo.micro_utils.common.compose"/> | ||||||
| @@ -1 +0,0 @@ | |||||||
| <manifest/> |  | ||||||
| @@ -1,76 +0,0 @@ | |||||||
| 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 |  | ||||||
| @@ -1,61 +0,0 @@ | |||||||
| 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) |  | ||||||
| @@ -1,7 +0,0 @@ | |||||||
| package dev.inmo.micro_utils.common |  | ||||||
|  |  | ||||||
| import android.app.Activity |  | ||||||
| import android.view.View |  | ||||||
|  |  | ||||||
| val Activity.rootView: View? |  | ||||||
|     get() = findViewById<View?>(android.R.id.content) ?.rootView ?: window.decorView.findViewById<View?>(android.R.id.content) ?.rootView |  | ||||||
| @@ -1,5 +1,3 @@ | |||||||
| @file:Suppress("OPT_IN_IS_NOT_ENABLED") |  | ||||||
|  |  | ||||||
| package dev.inmo.micro_utils.common | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
| @RequiresOptIn( | @RequiresOptIn( | ||||||
|   | |||||||
| @@ -2,8 +2,6 @@ | |||||||
|  |  | ||||||
| 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> | ||||||
| @@ -16,29 +14,16 @@ private inline fun <T> getObject( | |||||||
| /** | /** | ||||||
|  * Diff object which contains information about differences between two [Iterable]s |  * Diff object which contains information about differences between two [Iterable]s | ||||||
|  * |  * | ||||||
|  * See tests for more info |  | ||||||
|  * |  | ||||||
|  * @param removed The objects which has been presented in the old collection but absent in new one. Index here is the index in the old collection |  | ||||||
|  * @param added The object which appear in new collection only. Indexes here show the index in the new collection |  | ||||||
|  * @param replaced Pair of old-new changes. First object has been presented in the old collection on its |  | ||||||
|  * [IndexedValue.index] place, the second one is the object in new collection. Both have indexes due to the fact that in |  | ||||||
|  * case when some value has been replaced after adds or removes in original collection the object index will be changed |  | ||||||
|  * |  | ||||||
|  * @see calculateDiff |  * @see calculateDiff | ||||||
|  */ |  */ | ||||||
| @Serializable |  | ||||||
| data class Diff<T> internal constructor( | data class Diff<T> internal constructor( | ||||||
|     val removed: List<@Serializable(IndexedValueSerializer::class) IndexedValue<T>>, |     val removed: List<IndexedValue<T>>, | ||||||
|     /** |     /** | ||||||
|      * Old-New values pairs |      * Old-New values pairs | ||||||
|      */ |      */ | ||||||
|     val replaced: List<Pair<@Serializable(IndexedValueSerializer::class) IndexedValue<T>, @Serializable(IndexedValueSerializer::class) IndexedValue<T>>>, |     val replaced: List<Pair<IndexedValue<T>, IndexedValue<T>>>, | ||||||
|     val added: List<@Serializable(IndexedValueSerializer::class) IndexedValue<T>> |     val added: List<IndexedValue<T>> | ||||||
| ) { | ) | ||||||
|     fun isEmpty(): Boolean = removed.isEmpty() && replaced.isEmpty() && added.isEmpty() |  | ||||||
| } |  | ||||||
|  |  | ||||||
| fun <T> emptyDiff(): Diff<T> = Diff(emptyList(), emptyList(), emptyList()) |  | ||||||
|  |  | ||||||
| private inline fun <T> performChanges( | private inline fun <T> performChanges( | ||||||
|     potentialChanges: MutableList<Pair<IndexedValue<T>?, IndexedValue<T>?>>, |     potentialChanges: MutableList<Pair<IndexedValue<T>?, IndexedValue<T>?>>, | ||||||
| @@ -47,18 +32,17 @@ private inline fun <T> performChanges( | |||||||
|     changedList: MutableList<Pair<IndexedValue<T>, IndexedValue<T>>>, |     changedList: MutableList<Pair<IndexedValue<T>, IndexedValue<T>>>, | ||||||
|     removedList: MutableList<IndexedValue<T>>, |     removedList: MutableList<IndexedValue<T>>, | ||||||
|     addedList: MutableList<IndexedValue<T>>, |     addedList: MutableList<IndexedValue<T>>, | ||||||
|     comparisonFun: (T?, T?) -> Boolean |     strictComparison: 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 = comparisonFun(old ?.value, newObject ?.value) |         val oldOneEqualToNewObject = old ?.value === newObject ?.value || (old ?.value == newObject ?.value && !strictComparison) | ||||||
|         val newOneEqualToOldObject = comparisonFun(new ?.value, oldObject ?.value) |         val newOneEqualToOldObject = new ?.value === oldObject ?.value || (new ?.value == oldObject ?.value && !strictComparison) | ||||||
|         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 | ||||||
|                 } |                 } | ||||||
|             ) |             ) | ||||||
| @@ -108,7 +92,7 @@ private inline fun <T> performChanges( | |||||||
|  */ |  */ | ||||||
| fun <T> Iterable<T>.calculateDiff( | fun <T> Iterable<T>.calculateDiff( | ||||||
|     other: Iterable<T>, |     other: Iterable<T>, | ||||||
|     comparisonFun: (T?, T?) -> Boolean |     strictComparison: Boolean = false | ||||||
| ): Diff<T> { | ): Diff<T> { | ||||||
|     var i = -1 |     var i = -1 | ||||||
|     var j = -1 |     var j = -1 | ||||||
| @@ -136,60 +120,31 @@ fun <T> Iterable<T>.calculateDiff( | |||||||
|         } |         } | ||||||
|  |  | ||||||
|         when { |         when { | ||||||
|             comparisonFun(oldObject, newObject) -> { |             oldObject === newObject || (oldObject == newObject && !strictComparison) -> { | ||||||
|                 changedObjects.addAll(potentiallyChangedObjects.map { |                 changedObjects.addAll(potentiallyChangedObjects.map { it as Pair<IndexedValue<T>, IndexedValue<T>> }) | ||||||
|                     @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, comparisonFun) |                 performChanges(potentiallyChangedObjects, additionalInOld, additionalInNew, changedObjects, removedObjects, addedObjects, strictComparison) | ||||||
|                 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, comparisonFun) |     performChanges(potentiallyChangedObjects, additionalInOld, additionalInNew, changedObjects, removedObjects, addedObjects, strictComparison) | ||||||
|  |  | ||||||
|     return Diff(removedObjects.toList(), changedObjects.toList(), addedObjects.toList()) |     return Diff(removedObjects.toList(), changedObjects.toList(), addedObjects.toList()) | ||||||
| } | } | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Calculating [Diff] object |  | ||||||
|  * |  | ||||||
|  * @param strictComparison If this parameter set to true, objects which are not equal by links will be used as different |  | ||||||
|  * objects. For example, in case of two "Example" string they will be equal by value, but CAN be different by links |  | ||||||
|  */ |  | ||||||
| fun <T> Iterable<T>.calculateDiff( |  | ||||||
|     other: Iterable<T>, |  | ||||||
|     strictComparison: Boolean = false |  | ||||||
| ): Diff<T> = calculateDiff( |  | ||||||
|     other, |  | ||||||
|     comparisonFun = if (strictComparison) { |  | ||||||
|         { t1, t2 -> |  | ||||||
|             t1 === t2 |  | ||||||
|         } |  | ||||||
|     } else { |  | ||||||
|         { t1, t2 -> |  | ||||||
|             t1 === t2 || t1 == t2 // small optimization for cases when t1 and t2 are the same - comparison will be faster potentially |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| ) |  | ||||||
| inline fun <T> Iterable<T>.diff( | inline fun <T> Iterable<T>.diff( | ||||||
|     other: Iterable<T>, |     other: Iterable<T>, | ||||||
|     strictComparison: Boolean = false |     strictComparison: Boolean = false | ||||||
| ): Diff<T> = calculateDiff(other, strictComparison) | ): Diff<T> = calculateDiff(other, strictComparison) | ||||||
| inline fun <T> Iterable<T>.diff( |  | ||||||
|     other: Iterable<T>, |  | ||||||
|     noinline comparisonFun: (T?, T?) -> Boolean |  | ||||||
| ): Diff<T> = calculateDiff(other, comparisonFun) |  | ||||||
|  |  | ||||||
| inline fun <T> Diff(old: Iterable<T>, new: Iterable<T>) = old.calculateDiff(new, strictComparison = false) | inline fun <T> Diff(old: Iterable<T>, new: Iterable<T>) = old.calculateDiff(new) | ||||||
| inline fun <T> StrictDiff(old: Iterable<T>, new: Iterable<T>) = old.calculateDiff(new, true) | inline fun <T> StrictDiff(old: Iterable<T>, new: Iterable<T>) = old.calculateDiff(new, true) | ||||||
|  |  | ||||||
| /** | /** | ||||||
| @@ -199,23 +154,6 @@ inline fun <T> Iterable<T>.calculateStrictDiff( | |||||||
|     other: Iterable<T> |     other: Iterable<T> | ||||||
| ) = calculateDiff(other, strictComparison = true) | ) = calculateDiff(other, strictComparison = true) | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Applies [diff] to [this] [MutableList] |  | ||||||
|  */ |  | ||||||
| fun <T> MutableList<T>.applyDiff( |  | ||||||
|     diff: Diff<T> |  | ||||||
| ) { |  | ||||||
|     for (i in diff.removed.indices.sortedDescending()) { |  | ||||||
|         removeAt(diff.removed[i].index) |  | ||||||
|     } |  | ||||||
|     diff.added.forEach { (i, t) -> |  | ||||||
|         add(i, t) |  | ||||||
|     } |  | ||||||
|     diff.replaced.forEach { (_, new) -> |  | ||||||
|         set(new.index, new.value) |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * This method call [calculateDiff] with strict mode [strictComparison] and then apply differences to [this] |  * This method call [calculateDiff] with strict mode [strictComparison] and then apply differences to [this] | ||||||
|  * mutable list |  * mutable list | ||||||
| @@ -223,27 +161,14 @@ fun <T> MutableList<T>.applyDiff( | |||||||
| fun <T> MutableList<T>.applyDiff( | fun <T> MutableList<T>.applyDiff( | ||||||
|     source: Iterable<T>, |     source: Iterable<T>, | ||||||
|     strictComparison: Boolean = false |     strictComparison: Boolean = false | ||||||
| ): Diff<T> = calculateDiff(source, strictComparison).also { | ) = calculateDiff(source, strictComparison).let { | ||||||
|     applyDiff(it) |     for (i in it.removed.indices.sortedDescending()) { | ||||||
|  |         removeAt(it.removed[i].index) | ||||||
|  |     } | ||||||
|  |     it.added.forEach { (i, t) -> | ||||||
|  |         add(i, t) | ||||||
|  |     } | ||||||
|  |     it.replaced.forEach { (_, new) -> | ||||||
|  |         set(new.index, new.value) | ||||||
|  |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * This method call [calculateDiff] and then apply differences to [this] |  | ||||||
|  * mutable list |  | ||||||
|  */ |  | ||||||
| fun <T> MutableList<T>.applyDiff( |  | ||||||
|     source: Iterable<T>, |  | ||||||
|     comparisonFun: (T?, T?) -> Boolean |  | ||||||
| ): Diff<T> = calculateDiff(source, comparisonFun).also { |  | ||||||
|     applyDiff(it) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Reverse [this] [Diff]. Result will contain [Diff.added] on [Diff.removed] (and vice-verse), all the |  | ||||||
|  * [Diff.replaced] values will be reversed too |  | ||||||
|  */ |  | ||||||
| fun <T> Diff<T>.reversed() = Diff( |  | ||||||
|     removed = added, |  | ||||||
|     replaced = replaced.map { it.second to it.first }, |  | ||||||
|     added = removed |  | ||||||
| ) |  | ||||||
|   | |||||||
| @@ -1,5 +1,3 @@ | |||||||
| @file:Suppress("unused", "NOTHING_TO_INLINE") |  | ||||||
|  |  | ||||||
| package dev.inmo.micro_utils.common | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
| import kotlinx.serialization.* | import kotlinx.serialization.* | ||||||
| @@ -23,18 +21,26 @@ import kotlinx.serialization.encoding.* | |||||||
| sealed interface Either<T1, T2> { | sealed interface Either<T1, T2> { | ||||||
|     val optionalT1: Optional<T1> |     val optionalT1: Optional<T1> | ||||||
|     val optionalT2: Optional<T2> |     val optionalT2: Optional<T2> | ||||||
|  |     @Deprecated("Use optionalT1 instead", ReplaceWith("optionalT1")) | ||||||
|     val t1OrNull: T1? |     val t1: T1? | ||||||
|         get() = optionalT1.dataOrNull() |         get() = optionalT1.dataOrNull() | ||||||
|     val t2OrNull: T2? |     @Deprecated("Use optionalT2 instead", ReplaceWith("optionalT2")) | ||||||
|  |     val t2: T2? | ||||||
|         get() = optionalT2.dataOrNull() |         get() = optionalT2.dataOrNull() | ||||||
|  |  | ||||||
|  |     companion object { | ||||||
|  |         fun <T1, T2> serializer( | ||||||
|  |             t1Serializer: KSerializer<T1>, | ||||||
|  |             t2Serializer: KSerializer<T2>, | ||||||
|  |         ): KSerializer<Either<T1, T2>> = EitherSerializer(t1Serializer, t2Serializer) | ||||||
|  |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| class EitherSerializer<T1, T2>( | class EitherSerializer<T1, T2>( | ||||||
|     t1Serializer: KSerializer<T1>, |     t1Serializer: KSerializer<T1>, | ||||||
|     t2Serializer: KSerializer<T2>, |     t2Serializer: KSerializer<T2>, | ||||||
| ) : KSerializer<Either<T1, T2>> { | ) : KSerializer<Either<T1, T2>> { | ||||||
|     @OptIn(InternalSerializationApi::class, ExperimentalSerializationApi::class) |     @OptIn(ExperimentalSerializationApi::class, InternalSerializationApi::class) | ||||||
|     override val descriptor: SerialDescriptor = buildSerialDescriptor( |     override val descriptor: SerialDescriptor = buildSerialDescriptor( | ||||||
|         "TypedSerializer", |         "TypedSerializer", | ||||||
|         SerialKind.CONTEXTUAL |         SerialKind.CONTEXTUAL | ||||||
| @@ -97,7 +103,7 @@ class EitherSerializer<T1, T2>( | |||||||
|  */ |  */ | ||||||
| @Serializable | @Serializable | ||||||
| data class EitherFirst<T1, T2>( | data class EitherFirst<T1, T2>( | ||||||
|     val t1: T1 |     override val t1: T1 | ||||||
| ) : Either<T1, T2> { | ) : Either<T1, T2> { | ||||||
|     override val optionalT1: Optional<T1> = t1.optional |     override val optionalT1: Optional<T1> = t1.optional | ||||||
|     override val optionalT2: Optional<T2> = Optional.absent() |     override val optionalT2: Optional<T2> = Optional.absent() | ||||||
| @@ -108,7 +114,7 @@ data class EitherFirst<T1, T2>( | |||||||
|  */ |  */ | ||||||
| @Serializable | @Serializable | ||||||
| data class EitherSecond<T1, T2>( | data class EitherSecond<T1, T2>( | ||||||
|     val t2: T2 |     override val t2: T2 | ||||||
| ) : Either<T1, T2> { | ) : Either<T1, T2> { | ||||||
|     override val optionalT1: Optional<T1> = Optional.absent() |     override val optionalT1: Optional<T1> = Optional.absent() | ||||||
|     override val optionalT2: Optional<T2> = t2.optional |     override val optionalT2: Optional<T2> = t2.optional | ||||||
|   | |||||||
| @@ -1,43 +0,0 @@ | |||||||
| 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() |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,30 +0,0 @@ | |||||||
| package dev.inmo.micro_utils.common |  | ||||||
|  |  | ||||||
| import kotlinx.serialization.KSerializer |  | ||||||
| import kotlinx.serialization.Serializer |  | ||||||
| import kotlinx.serialization.builtins.PairSerializer |  | ||||||
| import kotlinx.serialization.builtins.serializer |  | ||||||
| import kotlinx.serialization.descriptors.SerialDescriptor |  | ||||||
| import kotlinx.serialization.encoding.Decoder |  | ||||||
| import kotlinx.serialization.encoding.Encoder |  | ||||||
|  |  | ||||||
| class IndexedValueSerializer<T>(private val subSerializer: KSerializer<T>) : KSerializer<IndexedValue<T>> { |  | ||||||
|     private val originalSerializer = PairSerializer(Int.serializer(), subSerializer) |  | ||||||
|     override val descriptor: SerialDescriptor |  | ||||||
|         get() = originalSerializer.descriptor |  | ||||||
|  |  | ||||||
|     override fun deserialize(decoder: Decoder): IndexedValue<T> { |  | ||||||
|         val pair = originalSerializer.deserialize(decoder) |  | ||||||
|         return IndexedValue( |  | ||||||
|             pair.first, |  | ||||||
|             pair.second |  | ||||||
|         ) |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     override fun serialize(encoder: Encoder, value: IndexedValue<T>) { |  | ||||||
|         originalSerializer.serialize( |  | ||||||
|             encoder, |  | ||||||
|             Pair(value.index, value.value) |  | ||||||
|         ) |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,135 +0,0 @@ | |||||||
| package dev.inmo.micro_utils.common |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Contains diff based on the comparison of objects with the same [K]. |  | ||||||
|  * |  | ||||||
|  * @param removed Contains map with keys removed from parent map |  | ||||||
|  * @param changed Contains map with keys values changed new map in comparison with old one |  | ||||||
|  * @param added Contains map with new keys and values |  | ||||||
|  */ |  | ||||||
| data class MapDiff<K, V> @Warning(warning) constructor( |  | ||||||
|     val removed: Map<K, V>, |  | ||||||
|     val changed: Map<K, Pair<V, V>>, |  | ||||||
|     val added: Map<K, V> |  | ||||||
| ) { |  | ||||||
|     fun isEmpty() = removed.isEmpty() && changed.isEmpty() && added.isEmpty() |  | ||||||
|     inline fun isNotEmpty() = !isEmpty() |  | ||||||
|  |  | ||||||
|     companion object { |  | ||||||
|         private const val warning = "This feature can be changed without any warranties. Use with caution and only in case you know what you are doing" |  | ||||||
|         fun <K, V> empty() = MapDiff<K, V>(emptyMap(), emptyMap(), emptyMap()) |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| private inline fun <K, V> createCompareFun( |  | ||||||
|     strictComparison: Boolean |  | ||||||
| ): (K, V, V) -> Boolean = if (strictComparison) { |  | ||||||
|     { _, first, second -> first === second } |  | ||||||
| } else { |  | ||||||
|     { _, first, second -> first == second } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Compare [this] [Map] with the [other] one in principle when [other] is newer than [this] |  | ||||||
|  * |  | ||||||
|  * @param compareFun Will be used to determine changed values |  | ||||||
|  */ |  | ||||||
| fun <K, V> Map<K, V>.diff( |  | ||||||
|     other: Map<K, V>, |  | ||||||
|     compareFun: (K, V, V) -> Boolean |  | ||||||
| ): MapDiff<K, V> { |  | ||||||
|     val removed: Map<K, V> = (keys - other.keys).associateWith { |  | ||||||
|         getValue(it) |  | ||||||
|     } |  | ||||||
|     val added: Map<K, V> = (other.keys - keys).associateWith { |  | ||||||
|         other.getValue(it) |  | ||||||
|     } |  | ||||||
|     val changed = keys.intersect(other.keys).mapNotNull { |  | ||||||
|         val old = getValue(it) |  | ||||||
|         val new = other.getValue(it) |  | ||||||
|         if (compareFun(it, old, new)) { |  | ||||||
|             return@mapNotNull null |  | ||||||
|         } else { |  | ||||||
|             it to (old to new) |  | ||||||
|         } |  | ||||||
|     }.toMap() |  | ||||||
|  |  | ||||||
|     return MapDiff( |  | ||||||
|         removed, |  | ||||||
|         changed, |  | ||||||
|         added |  | ||||||
|     ) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Compare [this] [Map] with the [other] one in principle when [other] is newer than [this] |  | ||||||
|  * |  | ||||||
|  * @param strictComparison If true, will use strict (===) comparison for the values' comparison. Otherwise, standard |  | ||||||
|  * `equals` will be used |  | ||||||
|  */ |  | ||||||
| fun <K, V> Map<K, V>.diff( |  | ||||||
|     other: Map<K, V>, |  | ||||||
|     strictComparison: Boolean = false |  | ||||||
| ): MapDiff<K, V> = diff( |  | ||||||
|     other, |  | ||||||
|     compareFun = createCompareFun(strictComparison) |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Will apply [mapDiff] to [this] [MutableMap] |  | ||||||
|  */ |  | ||||||
| fun <K, V> MutableMap<K, V>.applyDiff( |  | ||||||
|     mapDiff: MapDiff<K, V> |  | ||||||
| ) { |  | ||||||
|     mapDiff.apply { |  | ||||||
|         removed.keys.forEach { remove(it) } |  | ||||||
|         changed.forEach { (k, oldNew) -> |  | ||||||
|             put(k, oldNew.second) |  | ||||||
|         } |  | ||||||
|         added.forEach { (k, new) -> |  | ||||||
|             put(k, new) |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Will apply changes with [from] map into [this] one |  | ||||||
|  * |  | ||||||
|  * @param compareFun Will be used to determine changed values |  | ||||||
|  * |  | ||||||
|  * @return [MapDiff] applied to [this] [MutableMap] |  | ||||||
|  */ |  | ||||||
| fun <K, V> MutableMap<K, V>.applyDiff( |  | ||||||
|     from: Map<K, V>, |  | ||||||
|     compareFun: (K, V, V) -> Boolean |  | ||||||
| ): MapDiff<K, V> { |  | ||||||
|     return diff(from, compareFun).also { |  | ||||||
|         applyDiff(it) |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Will apply changes with [from] map into [this] one |  | ||||||
|  * |  | ||||||
|  * @param strictComparison If true, will use strict (===) comparison for the values' comparison. Otherwise, standard |  | ||||||
|  * `equals` will be used |  | ||||||
|  * |  | ||||||
|  * @return [MapDiff] applied to [this] [MutableMap] |  | ||||||
|  */ |  | ||||||
| fun <K, V> MutableMap<K, V>.applyDiff( |  | ||||||
|     from: Map<K, V>, |  | ||||||
|     strictComparison: Boolean = false |  | ||||||
| ): MapDiff<K, V> = applyDiff( |  | ||||||
|     from, |  | ||||||
|     compareFun = createCompareFun(strictComparison) |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Reverse [this] [MapDiff]. Result will contain [MapDiff.added] on [MapDiff.removed] (and vice-verse), all the |  | ||||||
|  * [MapDiff.changed] values will be reversed too |  | ||||||
|  */ |  | ||||||
| fun <K, V> MapDiff<K, V>.reversed(): MapDiff<K, V> = MapDiff( |  | ||||||
|     removed = added, |  | ||||||
|     changed = changed.mapValues { (_, oldNew) -> oldNew.second to oldNew.first }, |  | ||||||
|     added = removed |  | ||||||
| ) |  | ||||||
| @@ -1,53 +0,0 @@ | |||||||
| package dev.inmo.micro_utils.common |  | ||||||
|  |  | ||||||
| import kotlin.jvm.JvmName |  | ||||||
|  |  | ||||||
| interface SimpleMapper<T1, T2> { |  | ||||||
|     fun convertToT1(from: T2): T1 |  | ||||||
|     fun convertToT2(from: T1): T2 |  | ||||||
| } |  | ||||||
|  |  | ||||||
| @JvmName("convertFromT2") |  | ||||||
| fun <T1, T2> SimpleMapper<T1, T2>.convert(from: T2) = convertToT1(from) |  | ||||||
| @JvmName("convertFromT1") |  | ||||||
| fun <T1, T2> SimpleMapper<T1, T2>.convert(from: T1) = convertToT2(from) |  | ||||||
|  |  | ||||||
| class SimpleMapperImpl<T1, T2>( |  | ||||||
|     private val t1: (T2) -> T1, |  | ||||||
|     private val t2: (T1) -> T2, |  | ||||||
| ) : SimpleMapper<T1, T2> { |  | ||||||
|     override fun convertToT1(from: T2): T1 = t1.invoke(from) |  | ||||||
|  |  | ||||||
|     override fun convertToT2(from: T1): T2 = t2.invoke(from) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| @Suppress("NOTHING_TO_INLINE") |  | ||||||
| inline fun <T1, T2> simpleMapper( |  | ||||||
|     noinline t1: (T2) -> T1, |  | ||||||
|     noinline t2: (T1) -> T2, |  | ||||||
| ) = SimpleMapperImpl(t1, t2) |  | ||||||
|  |  | ||||||
| interface SimpleSuspendableMapper<T1, T2> { |  | ||||||
|     suspend fun convertToT1(from: T2): T1 |  | ||||||
|     suspend fun convertToT2(from: T1): T2 |  | ||||||
| } |  | ||||||
|  |  | ||||||
| @JvmName("convertFromT2") |  | ||||||
| suspend fun <T1, T2> SimpleSuspendableMapper<T1, T2>.convert(from: T2) = convertToT1(from) |  | ||||||
| @JvmName("convertFromT1") |  | ||||||
| suspend fun <T1, T2> SimpleSuspendableMapper<T1, T2>.convert(from: T1) = convertToT2(from) |  | ||||||
|  |  | ||||||
| class SimpleSuspendableMapperImpl<T1, T2>( |  | ||||||
|     private val t1: suspend (T2) -> T1, |  | ||||||
|     private val t2: suspend (T1) -> T2, |  | ||||||
| ) : SimpleSuspendableMapper<T1, T2> { |  | ||||||
|     override suspend fun convertToT1(from: T2): T1 = t1.invoke(from) |  | ||||||
|  |  | ||||||
|     override suspend fun convertToT2(from: T1): T2 = t2.invoke(from) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| @Suppress("NOTHING_TO_INLINE") |  | ||||||
| inline fun <T1, T2> simpleSuspendableMapper( |  | ||||||
|     noinline t1: suspend (T2) -> T1, |  | ||||||
|     noinline t2: suspend (T1) -> T2, |  | ||||||
| ) = SimpleSuspendableMapperImpl(t1, t2) |  | ||||||
| @@ -41,18 +41,10 @@ data class Optional<T> internal constructor( | |||||||
| inline val <T> T.optional | inline val <T> T.optional | ||||||
|     get() = Optional.presented(this) |     get() = Optional.presented(this) | ||||||
|  |  | ||||||
| inline val <T : Any> T?.optionalOrAbsentIfNull |  | ||||||
|     get() = if (this == null) { |  | ||||||
|         Optional.absent<T>() |  | ||||||
|     } else { |  | ||||||
|         Optional.presented(this) |  | ||||||
|     } |  | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Will call [block] when data presented ([Optional.dataPresented] == true) |  * Will call [block] when data presented ([Optional.dataPresented] == true) | ||||||
|  */ |  */ | ||||||
| inline fun <T> Optional<T>.onPresented(block: (T) -> Unit): Optional<T> = apply { | inline fun <T> Optional<T>.onPresented(block: (T) -> Unit): Optional<T> = apply { | ||||||
|     @OptIn(Warning::class) |  | ||||||
|     if (dataPresented) { @Suppress("UNCHECKED_CAST") block(data as T) } |     if (dataPresented) { @Suppress("UNCHECKED_CAST") block(data as T) } | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -60,7 +52,6 @@ inline fun <T> Optional<T>.onPresented(block: (T) -> Unit): Optional<T> = apply | |||||||
|  * Will call [block] when data presented ([Optional.dataPresented] == true) |  * Will call [block] when data presented ([Optional.dataPresented] == true) | ||||||
|  */ |  */ | ||||||
| inline fun <T, R> Optional<T>.mapOnPresented(block: (T) -> R): R? = run { | inline fun <T, R> Optional<T>.mapOnPresented(block: (T) -> R): R? = run { | ||||||
|     @OptIn(Warning::class) |  | ||||||
|     if (dataPresented) { @Suppress("UNCHECKED_CAST") block(data as T) } else null |     if (dataPresented) { @Suppress("UNCHECKED_CAST") block(data as T) } else null | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -68,7 +59,6 @@ inline fun <T, R> Optional<T>.mapOnPresented(block: (T) -> R): R? = run { | |||||||
|  * Will call [block] when data absent ([Optional.dataPresented] == false) |  * Will call [block] when data absent ([Optional.dataPresented] == false) | ||||||
|  */ |  */ | ||||||
| inline fun <T> Optional<T>.onAbsent(block: () -> Unit): Optional<T> = apply { | inline fun <T> Optional<T>.onAbsent(block: () -> Unit): Optional<T> = apply { | ||||||
|     @OptIn(Warning::class) |  | ||||||
|     if (!dataPresented) { block() } |     if (!dataPresented) { block() } | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -76,22 +66,27 @@ inline fun <T> Optional<T>.onAbsent(block: () -> Unit): Optional<T> = apply { | |||||||
|  * Will call [block] when data presented ([Optional.dataPresented] == true) |  * Will call [block] when data presented ([Optional.dataPresented] == true) | ||||||
|  */ |  */ | ||||||
| inline fun <T, R> Optional<T>.mapOnAbsent(block: () -> R): R? = run { | inline fun <T, R> Optional<T>.mapOnAbsent(block: () -> R): R? = run { | ||||||
|     @OptIn(Warning::class) |     if (!dataPresented) { @Suppress("UNCHECKED_CAST") block() } else null | ||||||
|     if (!dataPresented) { block() } else null |  | ||||||
| } | } | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Returns [Optional.data] if [Optional.dataPresented] of [this] is true, or null otherwise |  * Returns [Optional.data] if [Optional.dataPresented] of [this] is true, or null otherwise | ||||||
|  */ |  */ | ||||||
| fun <T> Optional<T>.dataOrNull() = @OptIn(Warning::class) if (dataPresented) @Suppress("UNCHECKED_CAST") (data as T) else null | fun <T> Optional<T>.dataOrNull() = if (dataPresented) @Suppress("UNCHECKED_CAST") (data as T) else null | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Returns [Optional.data] if [Optional.dataPresented] of [this] is true, or throw [throwable] otherwise |  * Returns [Optional.data] if [Optional.dataPresented] of [this] is true, or throw [throwable] otherwise | ||||||
|  */ |  */ | ||||||
| fun <T> Optional<T>.dataOrThrow(throwable: Throwable) = @OptIn(Warning::class) if (dataPresented) @Suppress("UNCHECKED_CAST") (data as T) else throw throwable | fun <T> Optional<T>.dataOrThrow(throwable: Throwable) = if (dataPresented) @Suppress("UNCHECKED_CAST") (data as T) else throw throwable | ||||||
|  |  | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Returns [Optional.data] if [Optional.dataPresented] of [this] is true, or call [block] and returns the result of it |  * Returns [Optional.data] if [Optional.dataPresented] of [this] is true, or call [block] and returns the result of it | ||||||
|  */ |  */ | ||||||
| inline fun <T> Optional<T>.dataOrElse(block: () -> T) = @OptIn(Warning::class) if (dataPresented) @Suppress("UNCHECKED_CAST") (data as T) else block() | inline fun <T> Optional<T>.dataOrElse(block: () -> T) = if (dataPresented) @Suppress("UNCHECKED_CAST") (data as T) else block() | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Returns [Optional.data] if [Optional.dataPresented] of [this] is true, or call [block] and returns the result of it | ||||||
|  |  */ | ||||||
|  | @Deprecated("dataOrElse now is inline", ReplaceWith("dataOrElse", "dev.inmo.micro_utils.common.dataOrElse")) | ||||||
|  | suspend fun <T> Optional<T>.dataOrElseSuspendable(block: suspend () -> T) = if (dataPresented) @Suppress("UNCHECKED_CAST") (data as T) else block() | ||||||
|   | |||||||
| @@ -1,28 +0,0 @@ | |||||||
| 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() |  | ||||||
| @@ -1,37 +0,0 @@ | |||||||
| package dev.inmo.micro_utils.common |  | ||||||
|  |  | ||||||
| import kotlinx.serialization.Serializable |  | ||||||
| import kotlin.jvm.JvmInline |  | ||||||
|  |  | ||||||
| @Serializable |  | ||||||
| @JvmInline |  | ||||||
| value class Progress private constructor( |  | ||||||
|     val of1: Double |  | ||||||
| ) { |  | ||||||
|     val of1Float |  | ||||||
|         get() = of1.toFloat() |  | ||||||
|     val of100 |  | ||||||
|         get() = of1 * 100 |  | ||||||
|     val of100Float |  | ||||||
|         get() = of100.toFloat() |  | ||||||
|     val of100Int |  | ||||||
|         get() = of100.toInt() |  | ||||||
|  |  | ||||||
|     init { |  | ||||||
|         require(of1 in rangeOfValues) { |  | ||||||
|             "Progress main value should be in $rangeOfValues, but incoming value is $of1" |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     companion object { |  | ||||||
|         val rangeOfValues = 0.0 .. 1.0 |  | ||||||
|  |  | ||||||
|         val START = Progress(rangeOfValues.start) |  | ||||||
|         val COMPLETED = Progress(rangeOfValues.endInclusive) |  | ||||||
|  |  | ||||||
|         operator fun invoke(of1: Double) = Progress(of1.coerceIn(rangeOfValues)) |  | ||||||
|         operator fun invoke(part: Number, total: Number) = Progress( |  | ||||||
|             part.toDouble() / total.toDouble() |  | ||||||
|         ) |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,80 +0,0 @@ | |||||||
| @file:Suppress( |  | ||||||
|   "RemoveRedundantCallsOfConversionMethods", |  | ||||||
|   "RedundantVisibilityModifier", |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| package dev.inmo.micro_utils.common |  | ||||||
|  |  | ||||||
| import kotlin.Byte |  | ||||||
| import kotlin.Double |  | ||||||
| import kotlin.Float |  | ||||||
| import kotlin.Int |  | ||||||
| import kotlin.Long |  | ||||||
| import kotlin.Short |  | ||||||
| import kotlin.Suppress |  | ||||||
|  |  | ||||||
| public operator fun Progress.plus(other: Progress): Progress = Progress(of1 + other.of1) |  | ||||||
|  |  | ||||||
| public operator fun Progress.minus(other: Progress): Progress = Progress(of1 - other.of1) |  | ||||||
|  |  | ||||||
| public operator fun Progress.plus(i: Byte): Progress = Progress((of1 + i).toDouble()) |  | ||||||
|  |  | ||||||
| public operator fun Progress.minus(i: Byte): Progress = Progress((of1 - i).toDouble()) |  | ||||||
|  |  | ||||||
| public operator fun Progress.times(i: Byte): Progress = Progress((of1 * i).toDouble()) |  | ||||||
|  |  | ||||||
| public operator fun Progress.div(i: Byte): Progress = Progress((of1 / i).toDouble()) |  | ||||||
|  |  | ||||||
| public operator fun Progress.rem(i: Byte): Progress = Progress((of1 % i).toDouble()) |  | ||||||
|  |  | ||||||
| public operator fun Progress.plus(i: Short): Progress = Progress((of1 + i).toDouble()) |  | ||||||
|  |  | ||||||
| public operator fun Progress.minus(i: Short): Progress = Progress((of1 - i).toDouble()) |  | ||||||
|  |  | ||||||
| public operator fun Progress.times(i: Short): Progress = Progress((of1 * i).toDouble()) |  | ||||||
|  |  | ||||||
| public operator fun Progress.div(i: Short): Progress = Progress((of1 / i).toDouble()) |  | ||||||
|  |  | ||||||
| public operator fun Progress.rem(i: Short): Progress = Progress((of1 % i).toDouble()) |  | ||||||
|  |  | ||||||
| public operator fun Progress.plus(i: Int): Progress = Progress((of1 + i).toDouble()) |  | ||||||
|  |  | ||||||
| public operator fun Progress.minus(i: Int): Progress = Progress((of1 - i).toDouble()) |  | ||||||
|  |  | ||||||
| public operator fun Progress.times(i: Int): Progress = Progress((of1 * i).toDouble()) |  | ||||||
|  |  | ||||||
| public operator fun Progress.div(i: Int): Progress = Progress((of1 / i).toDouble()) |  | ||||||
|  |  | ||||||
| public operator fun Progress.rem(i: Int): Progress = Progress((of1 % i).toDouble()) |  | ||||||
|  |  | ||||||
| public operator fun Progress.plus(i: Long): Progress = Progress((of1 + i).toDouble()) |  | ||||||
|  |  | ||||||
| public operator fun Progress.minus(i: Long): Progress = Progress((of1 - i).toDouble()) |  | ||||||
|  |  | ||||||
| public operator fun Progress.times(i: Long): Progress = Progress((of1 * i).toDouble()) |  | ||||||
|  |  | ||||||
| public operator fun Progress.div(i: Long): Progress = Progress((of1 / i).toDouble()) |  | ||||||
|  |  | ||||||
| public operator fun Progress.rem(i: Long): Progress = Progress((of1 % i).toDouble()) |  | ||||||
|  |  | ||||||
| public operator fun Progress.plus(i: Float): Progress = Progress((of1 + i).toDouble()) |  | ||||||
|  |  | ||||||
| public operator fun Progress.minus(i: Float): Progress = Progress((of1 - i).toDouble()) |  | ||||||
|  |  | ||||||
| public operator fun Progress.times(i: Float): Progress = Progress((of1 * i).toDouble()) |  | ||||||
|  |  | ||||||
| public operator fun Progress.div(i: Float): Progress = Progress((of1 / i).toDouble()) |  | ||||||
|  |  | ||||||
| public operator fun Progress.rem(i: Float): Progress = Progress((of1 % i).toDouble()) |  | ||||||
|  |  | ||||||
| public operator fun Progress.plus(i: Double): Progress = Progress((of1 + i).toDouble()) |  | ||||||
|  |  | ||||||
| public operator fun Progress.minus(i: Double): Progress = Progress((of1 - i).toDouble()) |  | ||||||
|  |  | ||||||
| public operator fun Progress.times(i: Double): Progress = Progress((of1 * i).toDouble()) |  | ||||||
|  |  | ||||||
| public operator fun Progress.div(i: Double): Progress = Progress((of1 / i).toDouble()) |  | ||||||
|  |  | ||||||
| public operator fun Progress.rem(i: Double): Progress = Progress((of1 % i).toDouble()) |  | ||||||
|  |  | ||||||
| public operator fun Progress.compareTo(other: Progress): Int = (of1 - other.of1).toInt() |  | ||||||
| @@ -1,27 +1,5 @@ | |||||||
| package dev.inmo.micro_utils.common | package dev.inmo.micro_utils.common | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Executes the given [action] until getting of successful result specified number of [times]. |  | ||||||
|  * |  | ||||||
|  * A zero-based index of current iteration is passed as a parameter to [action]. |  | ||||||
|  */ |  | ||||||
| inline fun <R> repeatOnFailure( |  | ||||||
|     onFailure: (Throwable) -> Boolean, |  | ||||||
|     action: () -> R |  | ||||||
| ): Result<R> { |  | ||||||
|     do { |  | ||||||
|         runCatching { |  | ||||||
|             action() |  | ||||||
|         }.onFailure { |  | ||||||
|             if (!onFailure(it)) { |  | ||||||
|                 return Result.failure(it) |  | ||||||
|             } |  | ||||||
|         }.onSuccess { |  | ||||||
|             return Result.success(it) |  | ||||||
|         } |  | ||||||
|     } while (true) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Executes the given [action] until getting of successful result specified number of [times]. |  * Executes the given [action] until getting of successful result specified number of [times]. | ||||||
|  * |  * | ||||||
| @@ -32,23 +10,12 @@ inline fun <R> repeatOnFailure( | |||||||
|     onEachFailure: (Throwable) -> Unit = {}, |     onEachFailure: (Throwable) -> Unit = {}, | ||||||
|     action: (Int) -> R |     action: (Int) -> R | ||||||
| ): Optional<R> { | ): Optional<R> { | ||||||
|     var i = 0 |     repeat(times) { | ||||||
|     val result = repeatOnFailure( |         runCatching { | ||||||
|         { |             action(it) | ||||||
|             onEachFailure(it) |         }.onFailure(onEachFailure).onSuccess { | ||||||
|             if (i < times) { |             return Optional.presented(it) | ||||||
|                 i++ |  | ||||||
|                 true |  | ||||||
|             } else { |  | ||||||
|                 false |  | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|     ) { |     return Optional.absent() | ||||||
|         action(i) |  | ||||||
|     } |  | ||||||
|     return if (result.isSuccess) { |  | ||||||
|         Optional.presented(result.getOrThrow()) |  | ||||||
|     } else { |  | ||||||
|         Optional.absent() |  | ||||||
|     } |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,6 +0,0 @@ | |||||||
| package dev.inmo.micro_utils.common |  | ||||||
|  |  | ||||||
| val FixedSignsRange = 0 .. 100 |  | ||||||
|  |  | ||||||
| expect fun Float.fixed(signs: Int): Float |  | ||||||
| expect fun Double.fixed(signs: Int): Double |  | ||||||
| @@ -32,7 +32,7 @@ class DiffUtilsTests { | |||||||
|         val withIndex = oldList.withIndex() |         val withIndex = oldList.withIndex() | ||||||
|  |  | ||||||
|         for (count in 1 .. (floor(oldList.size.toFloat() / 2).toInt())) { |         for (count in 1 .. (floor(oldList.size.toFloat() / 2).toInt())) { | ||||||
|             for ((i, _) in withIndex) { |             for ((i, v) in withIndex) { | ||||||
|                 if (i + count > oldList.lastIndex) { |                 if (i + count > oldList.lastIndex) { | ||||||
|                     continue |                     continue | ||||||
|                 } |                 } | ||||||
| @@ -55,7 +55,7 @@ class DiffUtilsTests { | |||||||
|         val withIndex = oldList.withIndex() |         val withIndex = oldList.withIndex() | ||||||
|  |  | ||||||
|         for (step in oldList.indices) { |         for (step in oldList.indices) { | ||||||
|             for ((i, _) in withIndex) { |             for ((i, v) in withIndex) { | ||||||
|                 val mutable = oldList.toMutableList() |                 val mutable = oldList.toMutableList() | ||||||
|                 val changes = ( |                 val changes = ( | ||||||
|                     if (step == 0) i until oldList.size else (i until oldList.size step step) |                     if (step == 0) i until oldList.size else (i until oldList.size step step) | ||||||
| @@ -104,7 +104,7 @@ class DiffUtilsTests { | |||||||
|         val withIndex = oldList.withIndex() |         val withIndex = oldList.withIndex() | ||||||
|  |  | ||||||
|         for (count in 1 .. (floor(oldList.size.toFloat() / 2).toInt())) { |         for (count in 1 .. (floor(oldList.size.toFloat() / 2).toInt())) { | ||||||
|             for ((i, _) in withIndex) { |             for ((i, v) in withIndex) { | ||||||
|                 if (i + count > oldList.lastIndex) { |                 if (i + count > oldList.lastIndex) { | ||||||
|                     continue |                     continue | ||||||
|                 } |                 } | ||||||
| @@ -129,20 +129,15 @@ class DiffUtilsTests { | |||||||
|         val withIndex = oldList.withIndex() |         val withIndex = oldList.withIndex() | ||||||
|  |  | ||||||
|         for (step in oldList.indices) { |         for (step in oldList.indices) { | ||||||
|             for ((i, _) in withIndex) { |             for ((i, v) in withIndex) { | ||||||
|                 val mutable = oldList.toMutableList() |                 val mutable = oldList.toMutableList() | ||||||
|  |                 val changes = ( | ||||||
|                 val newList = if (step == 0) { |                     if (step == 0) i until oldList.size else (i until oldList.size step step) | ||||||
|                     i until oldList.size |                 ).map { index -> | ||||||
|                 } else { |  | ||||||
|                     i until oldList.size step step |  | ||||||
|                 } |  | ||||||
|                 newList.forEach { index -> |  | ||||||
|                     IndexedValue(index, mutable[index]) to IndexedValue(index, "changed$index").also { |                     IndexedValue(index, mutable[index]) to IndexedValue(index, "changed$index").also { | ||||||
|                         mutable[index] = it.value |                         mutable[index] = it.value | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|  |  | ||||||
|                 val mutableOldList = oldList.toMutableList() |                 val mutableOldList = oldList.toMutableList() | ||||||
|                 mutableOldList.applyDiff(mutable) |                 mutableOldList.applyDiff(mutable) | ||||||
|                 assertEquals( |                 assertEquals( | ||||||
|   | |||||||
| @@ -1,12 +0,0 @@ | |||||||
| 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 |  | ||||||
| @@ -1,58 +0,0 @@ | |||||||
| 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" |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,4 +0,0 @@ | |||||||
| package dev.inmo.micro_utils.common |  | ||||||
|  |  | ||||||
| actual fun Float.fixed(signs: Int): Float = this.asDynamic().toFixed(signs.coerceIn(FixedSignsRange)).unsafeCast<String>().toFloat() |  | ||||||
| actual fun Double.fixed(signs: Int): Double = this.asDynamic().toFixed(signs.coerceIn(FixedSignsRange)).unsafeCast<String>().toDouble() |  | ||||||
| @@ -1,20 +0,0 @@ | |||||||
| package dev.inmo.micro_utils.common |  | ||||||
|  |  | ||||||
| import java.io.File |  | ||||||
| import java.io.InputStream |  | ||||||
| import java.util.UUID |  | ||||||
|  |  | ||||||
| fun InputStream.downloadToTempFile( |  | ||||||
|     fileName: String = UUID.randomUUID().toString(), |  | ||||||
|     fileExtension: String? = ".temp", |  | ||||||
|     folder: File? = null |  | ||||||
| ) = File.createTempFile( |  | ||||||
|     fileName, |  | ||||||
|     fileExtension, |  | ||||||
|     folder |  | ||||||
| ).apply { |  | ||||||
|     outputStream().use { |  | ||||||
|         copyTo(it) |  | ||||||
|     } |  | ||||||
|     deleteOnExit() |  | ||||||
| } |  | ||||||
| @@ -1,12 +0,0 @@ | |||||||
| package dev.inmo.micro_utils.common |  | ||||||
|  |  | ||||||
| import java.math.BigDecimal |  | ||||||
| import java.math.RoundingMode |  | ||||||
|  |  | ||||||
| actual fun Float.fixed(signs: Int): Float = BigDecimal.valueOf(this.toDouble()) |  | ||||||
|     .setScale(signs.coerceIn(FixedSignsRange), RoundingMode.HALF_UP) |  | ||||||
|     .toFloat(); |  | ||||||
|  |  | ||||||
| actual fun Double.fixed(signs: Int): Double = BigDecimal.valueOf(this) |  | ||||||
|     .setScale(signs.coerceIn(FixedSignsRange), RoundingMode.HALF_UP) |  | ||||||
|     .toDouble(); |  | ||||||
| @@ -1,36 +0,0 @@ | |||||||
| package dev.inmo.micro_utils.common |  | ||||||
|  |  | ||||||
| import okio.FileSystem |  | ||||||
| import okio.Path |  | ||||||
| import okio.use |  | ||||||
|  |  | ||||||
| actual typealias MPPFile = Path |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * @suppress |  | ||||||
|  */ |  | ||||||
| actual val MPPFile.filename: FileName |  | ||||||
|     get() = FileName(toString()) |  | ||||||
| /** |  | ||||||
|  * @suppress |  | ||||||
|  */ |  | ||||||
| actual val MPPFile.filesize: Long |  | ||||||
|     get() = FileSystem.SYSTEM.openReadOnly(this).use { |  | ||||||
|         it.size() |  | ||||||
|     } |  | ||||||
| /** |  | ||||||
|  * @suppress |  | ||||||
|  */ |  | ||||||
| actual val MPPFile.bytesAllocatorSync: ByteArrayAllocator |  | ||||||
|     get() = { |  | ||||||
|         FileSystem.SYSTEM.read(this) { |  | ||||||
|             readByteArray() |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| /** |  | ||||||
|  * @suppress |  | ||||||
|  */ |  | ||||||
| actual val MPPFile.bytesAllocator: SuspendByteArrayAllocator |  | ||||||
|     get() = { |  | ||||||
|         bytesAllocatorSync() |  | ||||||
|     } |  | ||||||
| @@ -1,25 +0,0 @@ | |||||||
| package dev.inmo.micro_utils.common |  | ||||||
|  |  | ||||||
| import kotlinx.cinterop.* |  | ||||||
| import platform.posix.snprintf |  | ||||||
| import platform.posix.sprintf |  | ||||||
|  |  | ||||||
| @OptIn(ExperimentalForeignApi::class) |  | ||||||
| actual fun Float.fixed(signs: Int): Float { |  | ||||||
|     return memScoped { |  | ||||||
|         val buff = allocArray<ByteVar>(Float.SIZE_BYTES * 2) |  | ||||||
|  |  | ||||||
|         sprintf(buff, "%.${signs}f", this@fixed) |  | ||||||
|         buff.toKString().toFloat() |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| @OptIn(ExperimentalForeignApi::class) |  | ||||||
| actual fun Double.fixed(signs: Int): Double { |  | ||||||
|     return memScoped { |  | ||||||
|         val buff = allocArray<ByteVar>(Double.SIZE_BYTES * 2) |  | ||||||
|  |  | ||||||
|         sprintf(buff, "%.${signs}f", this@fixed) |  | ||||||
|         buff.toKString().toDouble() |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,36 +0,0 @@ | |||||||
| package dev.inmo.micro_utils.common |  | ||||||
|  |  | ||||||
| import okio.FileSystem |  | ||||||
| import okio.Path |  | ||||||
| import okio.use |  | ||||||
|  |  | ||||||
| actual typealias MPPFile = Path |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * @suppress |  | ||||||
|  */ |  | ||||||
| actual val MPPFile.filename: FileName |  | ||||||
|     get() = FileName(toString()) |  | ||||||
| /** |  | ||||||
|  * @suppress |  | ||||||
|  */ |  | ||||||
| actual val MPPFile.filesize: Long |  | ||||||
|     get() = FileSystem.SYSTEM.openReadOnly(this).use { |  | ||||||
|         it.size() |  | ||||||
|     } |  | ||||||
| /** |  | ||||||
|  * @suppress |  | ||||||
|  */ |  | ||||||
| actual val MPPFile.bytesAllocatorSync: ByteArrayAllocator |  | ||||||
|     get() = { |  | ||||||
|         FileSystem.SYSTEM.read(this) { |  | ||||||
|             readByteArray() |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| /** |  | ||||||
|  * @suppress |  | ||||||
|  */ |  | ||||||
| actual val MPPFile.bytesAllocator: SuspendByteArrayAllocator |  | ||||||
|     get() = { |  | ||||||
|         bytesAllocatorSync() |  | ||||||
|     } |  | ||||||
| @@ -1,25 +0,0 @@ | |||||||
| package dev.inmo.micro_utils.common |  | ||||||
|  |  | ||||||
| import kotlinx.cinterop.* |  | ||||||
| import platform.posix.snprintf |  | ||||||
| import platform.posix.sprintf |  | ||||||
|  |  | ||||||
| @OptIn(ExperimentalForeignApi::class) |  | ||||||
| actual fun Float.fixed(signs: Int): Float { |  | ||||||
|     return memScoped { |  | ||||||
|         val buff = allocArray<ByteVar>(Float.SIZE_BYTES * 2) |  | ||||||
|  |  | ||||||
|         sprintf(buff, "%.${signs}f", this@fixed) |  | ||||||
|         buff.toKString().toFloat() |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| @OptIn(ExperimentalForeignApi::class) |  | ||||||
| actual fun Double.fixed(signs: Int): Double { |  | ||||||
|     return memScoped { |  | ||||||
|         val buff = allocArray<ByteVar>(Double.SIZE_BYTES * 2) |  | ||||||
|  |  | ||||||
|         sprintf(buff, "%.${signs}f", this@fixed) |  | ||||||
|         buff.toKString().toDouble() |  | ||||||
|     } |  | ||||||
| } |  | ||||||
							
								
								
									
										1
									
								
								common/src/main/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								common/src/main/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | <manifest package="dev.inmo.micro_utils.common"/> | ||||||
| @@ -1,36 +0,0 @@ | |||||||
| package dev.inmo.micro_utils.common |  | ||||||
|  |  | ||||||
| import okio.FileSystem |  | ||||||
| import okio.Path |  | ||||||
| import okio.use |  | ||||||
|  |  | ||||||
| actual typealias MPPFile = Path |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * @suppress |  | ||||||
|  */ |  | ||||||
| actual val MPPFile.filename: FileName |  | ||||||
|     get() = FileName(toString()) |  | ||||||
| /** |  | ||||||
|  * @suppress |  | ||||||
|  */ |  | ||||||
| actual val MPPFile.filesize: Long |  | ||||||
|     get() = FileSystem.SYSTEM.openReadOnly(this).use { |  | ||||||
|         it.size() |  | ||||||
|     } |  | ||||||
| /** |  | ||||||
|  * @suppress |  | ||||||
|  */ |  | ||||||
| actual val MPPFile.bytesAllocatorSync: ByteArrayAllocator |  | ||||||
|     get() = { |  | ||||||
|         FileSystem.SYSTEM.read(this) { |  | ||||||
|             readByteArray() |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| /** |  | ||||||
|  * @suppress |  | ||||||
|  */ |  | ||||||
| actual val MPPFile.bytesAllocator: SuspendByteArrayAllocator |  | ||||||
|     get() = { |  | ||||||
|         bytesAllocatorSync() |  | ||||||
|     } |  | ||||||
| @@ -1,25 +0,0 @@ | |||||||
| package dev.inmo.micro_utils.common |  | ||||||
|  |  | ||||||
| import kotlinx.cinterop.* |  | ||||||
| import platform.posix.snprintf |  | ||||||
| import platform.posix.sprintf |  | ||||||
|  |  | ||||||
| @OptIn(ExperimentalForeignApi::class) |  | ||||||
| actual fun Float.fixed(signs: Int): Float { |  | ||||||
|     return memScoped { |  | ||||||
|         val buff = allocArray<ByteVar>(Float.SIZE_BYTES * 2) |  | ||||||
|  |  | ||||||
|         sprintf(buff, "%.${signs}f", this@fixed) |  | ||||||
|         buff.toKString().toFloat() |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| @OptIn(ExperimentalForeignApi::class) |  | ||||||
| actual fun Double.fixed(signs: Int): Double { |  | ||||||
|     return memScoped { |  | ||||||
|         val buff = allocArray<ByteVar>(Double.SIZE_BYTES * 2) |  | ||||||
|  |  | ||||||
|         sprintf(buff, "%.${signs}f", this@fixed) |  | ||||||
|         buff.toKString().toDouble() |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -4,7 +4,7 @@ plugins { | |||||||
|     id "com.android.library" |     id "com.android.library" | ||||||
| } | } | ||||||
|  |  | ||||||
| apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64ProjectPresetPath" | apply from: "$mppProjectWithSerializationPresetPath" | ||||||
|  |  | ||||||
| kotlin { | kotlin { | ||||||
|     sourceSets { |     sourceSets { | ||||||
| @@ -22,7 +22,6 @@ kotlin { | |||||||
|             dependencies { |             dependencies { | ||||||
|                 api libs.kt.coroutines.android |                 api libs.kt.coroutines.android | ||||||
|             } |             } | ||||||
|             dependsOn(jvmMain) |  | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1 +0,0 @@ | |||||||
| <manifest/> |  | ||||||
| @@ -1,60 +0,0 @@ | |||||||
| package dev.inmo.micro_utils.coroutines.compose |  | ||||||
|  |  | ||||||
| import androidx.compose.runtime.* |  | ||||||
| import androidx.compose.runtime.snapshots.SnapshotStateList |  | ||||||
| import dev.inmo.micro_utils.common.applyDiff |  | ||||||
| import dev.inmo.micro_utils.coroutines.ExceptionHandler |  | ||||||
| import dev.inmo.micro_utils.coroutines.defaultSafelyWithoutExceptionHandlerWithNull |  | ||||||
| import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions |  | ||||||
| import kotlinx.coroutines.CoroutineScope |  | ||||||
| import kotlinx.coroutines.Dispatchers |  | ||||||
| import kotlinx.coroutines.flow.Flow |  | ||||||
| import kotlinx.coroutines.flow.StateFlow |  | ||||||
| import kotlinx.coroutines.withContext |  | ||||||
| import kotlin.coroutines.CoroutineContext |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Each value of [this] [Flow] will trigger [applyDiff] to the result [SnapshotStateList] |  | ||||||
|  * |  | ||||||
|  * @param scope Will be used to [subscribeSafelyWithoutExceptions] on [this] to update returned [SnapshotStateList] |  | ||||||
|  * @param useContextOnChange Will be used to change context inside of [subscribeSafelyWithoutExceptions] to ensure that |  | ||||||
|  * change will happen in the required [CoroutineContext]. [Dispatchers.Main] by default |  | ||||||
|  * @param onException Will be passed to the [subscribeSafelyWithoutExceptions] as uncaught exceptions handler |  | ||||||
|  */ |  | ||||||
| @Suppress("NOTHING_TO_INLINE") |  | ||||||
| inline fun <reified T> Flow<List<T>>.asMutableComposeListState( |  | ||||||
|     scope: CoroutineScope, |  | ||||||
|     useContextOnChange: CoroutineContext? = Dispatchers.Main, |  | ||||||
|     noinline onException: ExceptionHandler<List<T>?> = defaultSafelyWithoutExceptionHandlerWithNull, |  | ||||||
| ): SnapshotStateList<T> { |  | ||||||
|     val state = mutableStateListOf<T>() |  | ||||||
|     val changeBlock: suspend (List<T>) -> Unit = useContextOnChange ?.let { |  | ||||||
|         { |  | ||||||
|             withContext(useContextOnChange) { |  | ||||||
|                 state.applyDiff(it) |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } ?: { |  | ||||||
|         state.applyDiff(it) |  | ||||||
|     } |  | ||||||
|     subscribeSafelyWithoutExceptions(scope, onException, changeBlock) |  | ||||||
|     return state |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * In fact, it is just classcast of [asMutableComposeListState] to [List] |  | ||||||
|  * |  | ||||||
|  * @param scope Will be used to [subscribeSafelyWithoutExceptions] on [this] to update returned [List] |  | ||||||
|  * @param useContextOnChange Will be used to change context inside of [subscribeSafelyWithoutExceptions] to ensure that |  | ||||||
|  * change will happen in the required [CoroutineContext]. [Dispatchers.Main] by default |  | ||||||
|  * @param onException Will be passed to the [subscribeSafelyWithoutExceptions] as uncaught exceptions handler |  | ||||||
|  * |  | ||||||
|  * @return Changing in time [List] which follow [Flow] values |  | ||||||
|  */ |  | ||||||
| @Suppress("NOTHING_TO_INLINE") |  | ||||||
| inline fun <reified T> Flow<List<T>>.asComposeList( |  | ||||||
|     scope: CoroutineScope, |  | ||||||
|     useContextOnChange: CoroutineContext? = Dispatchers.Main, |  | ||||||
|     noinline onException: ExceptionHandler<List<T>?> = defaultSafelyWithoutExceptionHandlerWithNull, |  | ||||||
| ): List<T> = asMutableComposeListState(scope, useContextOnChange, onException) |  | ||||||
|  |  | ||||||
| @@ -1,94 +0,0 @@ | |||||||
| package dev.inmo.micro_utils.coroutines.compose |  | ||||||
|  |  | ||||||
| import androidx.compose.runtime.* |  | ||||||
| import dev.inmo.micro_utils.common.compose.asState |  | ||||||
| import dev.inmo.micro_utils.coroutines.ExceptionHandler |  | ||||||
| import dev.inmo.micro_utils.coroutines.defaultSafelyWithoutExceptionHandlerWithNull |  | ||||||
| import dev.inmo.micro_utils.coroutines.doInUI |  | ||||||
| import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions |  | ||||||
| import kotlinx.coroutines.CoroutineScope |  | ||||||
| import kotlinx.coroutines.Dispatchers |  | ||||||
| import kotlinx.coroutines.flow.Flow |  | ||||||
| import kotlinx.coroutines.flow.StateFlow |  | ||||||
| import kotlinx.coroutines.withContext |  | ||||||
| import kotlin.coroutines.CoroutineContext |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Will map [this] [Flow] as [MutableState]. Returned [MutableState] WILL NOT change source [Flow] |  | ||||||
|  * |  | ||||||
|  * @param initial First value which will be passed to the result [MutableState] |  | ||||||
|  * @param scope Will be used to [subscribeSafelyWithoutExceptions] on [this] to update returned [MutableState] |  | ||||||
|  * @param useContextOnChange Will be used to change context inside of [subscribeSafelyWithoutExceptions] to ensure that |  | ||||||
|  * change will happen in the required [CoroutineContext]. [Dispatchers.Main] by default |  | ||||||
|  * @param onException Will be passed to the [subscribeSafelyWithoutExceptions] as uncaught exceptions handler |  | ||||||
|  */ |  | ||||||
| fun <T> Flow<T>.asMutableComposeState( |  | ||||||
|     initial: T, |  | ||||||
|     scope: CoroutineScope, |  | ||||||
|     useContextOnChange: CoroutineContext? = Dispatchers.Main, |  | ||||||
|     onException: ExceptionHandler<T?> = defaultSafelyWithoutExceptionHandlerWithNull, |  | ||||||
| ): MutableState<T> { |  | ||||||
|     val state = mutableStateOf(initial) |  | ||||||
|     val changeBlock: suspend (T) -> Unit = useContextOnChange ?.let { |  | ||||||
|         { |  | ||||||
|             withContext(useContextOnChange) { |  | ||||||
|                 state.value = it |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } ?: { |  | ||||||
|         state.value = it |  | ||||||
|     } |  | ||||||
|     subscribeSafelyWithoutExceptions(scope, onException, block = changeBlock) |  | ||||||
|     return state |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Will map [this] [StateFlow] as [MutableState]. Returned [MutableState] WILL NOT change source [StateFlow]. |  | ||||||
|  * This conversation will pass its [StateFlow.value] as the first value |  | ||||||
|  * |  | ||||||
|  * @param scope Will be used to [subscribeSafelyWithoutExceptions] on [this] to update returned [MutableState] |  | ||||||
|  * @param useContextOnChange Will be used to change context inside of [subscribeSafelyWithoutExceptions] to ensure that |  | ||||||
|  * change will happen in the required [CoroutineContext]. [Dispatchers.Main] by default |  | ||||||
|  * @param onException Will be passed to the [subscribeSafelyWithoutExceptions] as uncaught exceptions handler |  | ||||||
|  */ |  | ||||||
| @Suppress("NOTHING_TO_INLINE") |  | ||||||
| inline fun <T> StateFlow<T>.asMutableComposeState( |  | ||||||
|     scope: CoroutineScope, |  | ||||||
|     useContextOnChange: CoroutineContext? = Dispatchers.Main, |  | ||||||
|     noinline onException: ExceptionHandler<T?> = defaultSafelyWithoutExceptionHandlerWithNull, |  | ||||||
| ): MutableState<T> = asMutableComposeState(value, scope, useContextOnChange, onException) |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Will create [MutableState] using [asMutableComposeState] and use [asState] to convert it as immutable state |  | ||||||
|  * |  | ||||||
|  * @param initial First value which will be passed to the result [State] |  | ||||||
|  * @param scope Will be used to [subscribeSafelyWithoutExceptions] on [this] to update returned [State] |  | ||||||
|  * @param useContextOnChange Will be used to change context inside of [subscribeSafelyWithoutExceptions] to ensure that |  | ||||||
|  * change will happen in the required [CoroutineContext]. [Dispatchers.Main] by default |  | ||||||
|  * @param onException Will be passed to the [subscribeSafelyWithoutExceptions] as uncaught exceptions handler |  | ||||||
|  */ |  | ||||||
| fun <T> Flow<T>.asComposeState( |  | ||||||
|     initial: T, |  | ||||||
|     scope: CoroutineScope, |  | ||||||
|     useContextOnChange: CoroutineContext? = Dispatchers.Main, |  | ||||||
|     onException: ExceptionHandler<T?> = defaultSafelyWithoutExceptionHandlerWithNull, |  | ||||||
| ): State<T> { |  | ||||||
|     val state = asMutableComposeState(initial, scope, useContextOnChange, onException) |  | ||||||
|     return state.asState() |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Will map [this] [StateFlow] as [State]. This conversation will pass its [StateFlow.value] as the first value |  | ||||||
|  * |  | ||||||
|  * @param scope Will be used to [subscribeSafelyWithoutExceptions] on [this] to update returned [State] |  | ||||||
|  * @param useContextOnChange Will be used to change context inside of [subscribeSafelyWithoutExceptions] to ensure that |  | ||||||
|  * change will happen in the required [CoroutineContext]. [Dispatchers.Main] by default |  | ||||||
|  * @param onException Will be passed to the [subscribeSafelyWithoutExceptions] as uncaught exceptions handler |  | ||||||
|  */ |  | ||||||
| @Suppress("NOTHING_TO_INLINE") |  | ||||||
| inline fun <T> StateFlow<T>.asComposeState( |  | ||||||
|     scope: CoroutineScope, |  | ||||||
|     useContextOnChange: CoroutineContext? = Dispatchers.Main, |  | ||||||
|     noinline onException: ExceptionHandler<T?> = defaultSafelyWithoutExceptionHandlerWithNull, |  | ||||||
| ): State<T> = asComposeState(value, scope, useContextOnChange, onException) |  | ||||||
|  |  | ||||||
| @@ -1,46 +0,0 @@ | |||||||
| package dev.inmo.micro_utils.coroutines.compose |  | ||||||
|  |  | ||||||
| import androidx.compose.runtime.MutableState |  | ||||||
| import dev.inmo.micro_utils.coroutines.SpecialMutableStateFlow |  | ||||||
| import kotlinx.coroutines.CoroutineScope |  | ||||||
| import kotlinx.coroutines.Dispatchers |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * This type works like [MutableState], [kotlinx.coroutines.flow.StateFlow] and [kotlinx.coroutines.flow.MutableSharedFlow]. |  | ||||||
|  * Based on [SpecialMutableStateFlow] |  | ||||||
|  */ |  | ||||||
| @Deprecated("Will be removed soon") |  | ||||||
| class FlowState<T>( |  | ||||||
|     initial: T, |  | ||||||
|     internalScope: CoroutineScope = CoroutineScope(Dispatchers.Default) |  | ||||||
| ) : MutableState<T>, |  | ||||||
|     SpecialMutableStateFlow<T>(initial, internalScope) { |  | ||||||
|     private var internalValue: T = initial |  | ||||||
|     override var value: T |  | ||||||
|         get() = internalValue |  | ||||||
|         set(value) { |  | ||||||
|             internalValue = value |  | ||||||
|             tryEmit(value) |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|     override fun onChangeWithoutSync(value: T) { |  | ||||||
|         internalValue = value |  | ||||||
|         super.onChangeWithoutSync(value) |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     override fun component1(): T = value |  | ||||||
|  |  | ||||||
|     override fun component2(): (T) -> Unit = { tryEmit(it) } |  | ||||||
|  |  | ||||||
|     override fun tryEmit(value: T): Boolean { |  | ||||||
|         internalValue = value |  | ||||||
|         return super.tryEmit(value) |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     override suspend fun emit(value: T) { |  | ||||||
|         internalValue = value |  | ||||||
|         super.emit(value) |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| //fun <T> MutableState<T>.asFlowState(scope: CoroutineScope = CoroutineScope(Dispatchers.Main)) = FlowState(this, scope) |  | ||||||
| @@ -0,0 +1,22 @@ | |||||||
|  | package dev.inmo.micro_utils.coroutines.compose | ||||||
|  |  | ||||||
|  | import androidx.compose.runtime.MutableState | ||||||
|  | import androidx.compose.runtime.mutableStateOf | ||||||
|  | import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions | ||||||
|  | import kotlinx.coroutines.CoroutineScope | ||||||
|  | import kotlinx.coroutines.flow.Flow | ||||||
|  | import kotlinx.coroutines.flow.StateFlow | ||||||
|  |  | ||||||
|  | fun <T> Flow<T>.toMutableState( | ||||||
|  |     initial: T, | ||||||
|  |     scope: CoroutineScope | ||||||
|  | ): MutableState<T> { | ||||||
|  |     val state = mutableStateOf(initial) | ||||||
|  |     subscribeSafelyWithoutExceptions(scope) { state.value = it } | ||||||
|  |     return state | ||||||
|  | } | ||||||
|  |  | ||||||
|  | inline fun <T> StateFlow<T>.toMutableState( | ||||||
|  |     scope: CoroutineScope | ||||||
|  | ): MutableState<T> = toMutableState(value, scope) | ||||||
|  |  | ||||||
							
								
								
									
										1
									
								
								coroutines/compose/src/main/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								coroutines/compose/src/main/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | <manifest package="dev.inmo.micro_utils.coroutines.compose"/> | ||||||
| @@ -1 +0,0 @@ | |||||||
| <manifest/> |  | ||||||
| @@ -1,50 +0,0 @@ | |||||||
| package dev.inmo.micro_utils.coroutines |  | ||||||
|  |  | ||||||
| import android.view.View |  | ||||||
| import android.view.ViewGroup |  | ||||||
| import kotlinx.coroutines.flow.MutableSharedFlow |  | ||||||
| import kotlinx.coroutines.flow.asSharedFlow |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * [kotlinx.coroutines.flow.Flow]-based [android.view.ViewGroup.OnHierarchyChangeListener] |  | ||||||
|  * |  | ||||||
|  * @param recursive If set, any call of [onChildViewAdded] will check if child [View] is [ViewGroup] and subscribe to this |  | ||||||
|  * [ViewGroup] too |  | ||||||
|  * @param [_onChildViewAdded] Internal [MutableSharedFlow] which will be used to pass data to [onChildViewAdded] flow |  | ||||||
|  * @param [_onChildViewRemoved] Internal [MutableSharedFlow] which will be used to pass data to [onChildViewRemoved] flow |  | ||||||
|  */ |  | ||||||
| class FlowOnHierarchyChangeListener( |  | ||||||
|     private val recursive: Boolean = false, |  | ||||||
|     private val _onChildViewAdded: MutableSharedFlow<Pair<View, View>> = MutableSharedFlow(extraBufferCapacity = Int.MAX_VALUE), |  | ||||||
|     private val _onChildViewRemoved: MutableSharedFlow<Pair<View, View>> = MutableSharedFlow(extraBufferCapacity = Int.MAX_VALUE) |  | ||||||
| ) : ViewGroup.OnHierarchyChangeListener { |  | ||||||
|     val onChildViewAdded = _onChildViewAdded.asSharedFlow() |  | ||||||
|     val onChildViewRemoved = _onChildViewRemoved.asSharedFlow() |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Will emit data into [onChildViewAdded] flow. If [recursive] is true and [child] is [ViewGroup] will also |  | ||||||
|      * subscribe to [child] hierarchy changes. |  | ||||||
|      * |  | ||||||
|      * Due to the fact that this method is not suspendable, [FlowOnHierarchyChangeListener] will use |  | ||||||
|      * [MutableSharedFlow.tryEmit] to send data into [_onChildViewAdded]. That is why its default extraBufferCapacity is |  | ||||||
|      * [Int.MAX_VALUE] |  | ||||||
|      */ |  | ||||||
|     override fun onChildViewAdded(parent: View, child: View) { |  | ||||||
|         _onChildViewAdded.tryEmit(parent to child) |  | ||||||
|  |  | ||||||
|         if (recursive && child is ViewGroup) { |  | ||||||
|             child.setOnHierarchyChangeListener(this) |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Just emit data into [onChildViewRemoved] |  | ||||||
|      * |  | ||||||
|      * Due to the fact that this method is not suspendable, [FlowOnHierarchyChangeListener] will use |  | ||||||
|      * [MutableSharedFlow.tryEmit] to send data into [_onChildViewRemoved]. That is why its default extraBufferCapacity is |  | ||||||
|      * [Int.MAX_VALUE] |  | ||||||
|      */ |  | ||||||
|     override fun onChildViewRemoved(parent: View, child: View) { |  | ||||||
|         _onChildViewRemoved.tryEmit(parent to child) |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,17 +0,0 @@ | |||||||
| package dev.inmo.micro_utils.coroutines |  | ||||||
|  |  | ||||||
| import android.view.ViewGroup |  | ||||||
| import android.view.ViewGroup.OnHierarchyChangeListener |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Use [ViewGroup.setOnHierarchyChangeListener] recursively for all available [ViewGroup]s starting with [this]. |  | ||||||
|  * This extension DO NOT guarantee that recursive subscription will happen after this method call |  | ||||||
|  */ |  | ||||||
| fun ViewGroup.setOnHierarchyChangeListenerRecursively( |  | ||||||
|     listener: OnHierarchyChangeListener |  | ||||||
| ) { |  | ||||||
|     setOnHierarchyChangeListener(listener) |  | ||||||
|     (0 until childCount).forEach { |  | ||||||
|         (getChildAt(it) as? ViewGroup) ?.setOnHierarchyChangeListenerRecursively(listener) |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -6,12 +6,11 @@ import kotlinx.coroutines.channels.Channel | |||||||
| import kotlinx.coroutines.flow.* | import kotlinx.coroutines.flow.* | ||||||
| import kotlinx.coroutines.sync.Mutex | import kotlinx.coroutines.sync.Mutex | ||||||
| import kotlinx.coroutines.sync.withLock | import kotlinx.coroutines.sync.withLock | ||||||
| import kotlin.coroutines.cancellation.CancellationException |  | ||||||
|  |  | ||||||
| private sealed interface AccumulatorFlowStep<T> | private sealed interface AccumulatorFlowStep | ||||||
| private data class DataRetrievedAccumulatorFlowStep<T>(val data: T) : AccumulatorFlowStep<T> | private data class DataRetrievedAccumulatorFlowStep(val data: Any) : AccumulatorFlowStep | ||||||
| private data class SubscribeAccumulatorFlowStep<T>(val channel: Channel<T>) : AccumulatorFlowStep<T> | private data class SubscribeAccumulatorFlowStep(val channel: Channel<Any>) : AccumulatorFlowStep | ||||||
| private data class UnsubscribeAccumulatorFlowStep<T>(val channel: Channel<T>) : AccumulatorFlowStep<T> | private data class UnsubscribeAccumulatorFlowStep(val channel: Channel<Any>) : AccumulatorFlowStep | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * This [Flow] will have behaviour very similar to [SharedFlow], but there are several differences: |  * This [Flow] will have behaviour very similar to [SharedFlow], but there are several differences: | ||||||
| @@ -27,12 +26,12 @@ class AccumulatorFlow<T>( | |||||||
|     private val subscope = scope.LinkedSupervisorScope() |     private val subscope = scope.LinkedSupervisorScope() | ||||||
|     private val activeData = ArrayDeque<T>() |     private val activeData = ArrayDeque<T>() | ||||||
|     private val dataMutex = Mutex() |     private val dataMutex = Mutex() | ||||||
|     private val channelsForBroadcast = mutableListOf<Channel<T>>() |     private val channelsForBroadcast = mutableListOf<Channel<Any>>() | ||||||
|     private val channelsMutex = Mutex() |     private val channelsMutex = Mutex() | ||||||
|     private val steps = subscope.actor<AccumulatorFlowStep<T>> { step -> |     private val steps = subscope.actor<AccumulatorFlowStep> { step -> | ||||||
|         when (step) { |         when (step) { | ||||||
|             is DataRetrievedAccumulatorFlowStep -> { |             is DataRetrievedAccumulatorFlowStep -> { | ||||||
|                 if (activeData.firstOrNull() === step.data) { |                 if (activeData.first() === step.data) { | ||||||
|                     dataMutex.withLock { |                     dataMutex.withLock { | ||||||
|                         activeData.removeFirst() |                         activeData.removeFirst() | ||||||
|                     } |                     } | ||||||
| @@ -43,7 +42,7 @@ class AccumulatorFlow<T>( | |||||||
|                 dataMutex.withLock { |                 dataMutex.withLock { | ||||||
|                     val dataToSend = activeData.toList() |                     val dataToSend = activeData.toList() | ||||||
|                     safelyWithoutExceptions { |                     safelyWithoutExceptions { | ||||||
|                         dataToSend.forEach { step.channel.send(it) } |                         dataToSend.forEach { step.channel.send(it as Any) } | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
| @@ -59,29 +58,24 @@ class AccumulatorFlow<T>( | |||||||
|         channelsMutex.withLock { |         channelsMutex.withLock { | ||||||
|             channelsForBroadcast.forEach { channel -> |             channelsForBroadcast.forEach { channel -> | ||||||
|                 safelyWithResult { |                 safelyWithResult { | ||||||
|                     channel.send(it) |                     channel.send(it as Any) | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     override suspend fun collectSafely(collector: FlowCollector<T>) { |     override suspend fun collectSafely(collector: FlowCollector<T>) { | ||||||
|         val channel = Channel<T>(Channel.UNLIMITED, BufferOverflow.SUSPEND) |         val channel = Channel<Any>(Channel.UNLIMITED, BufferOverflow.SUSPEND) | ||||||
|         steps.send(SubscribeAccumulatorFlowStep(channel)) |         steps.send(SubscribeAccumulatorFlowStep(channel)) | ||||||
|         val result = runCatchingSafely { |  | ||||||
|         for (data in channel) { |         for (data in channel) { | ||||||
|                 val emitResult = runCatchingSafely { |             try { | ||||||
|                     collector.emit(data) |                 collector.emit(data as T) | ||||||
|                 } |  | ||||||
|                 if (emitResult.isSuccess || emitResult.exceptionOrNull() is CancellationException) { |  | ||||||
|                 steps.send(DataRetrievedAccumulatorFlowStep(data)) |                 steps.send(DataRetrievedAccumulatorFlowStep(data)) | ||||||
|                 } |             } finally { | ||||||
|                 emitResult.getOrThrow() |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|                 channel.cancel() |                 channel.cancel() | ||||||
|                 steps.send(UnsubscribeAccumulatorFlowStep(channel)) |                 steps.send(UnsubscribeAccumulatorFlowStep(channel)) | ||||||
|         result.getOrThrow() |             } | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,15 +1,19 @@ | |||||||
| package dev.inmo.micro_utils.coroutines | package dev.inmo.micro_utils.coroutines | ||||||
|  |  | ||||||
| import kotlinx.coroutines.* | import kotlinx.coroutines.CoroutineScope | ||||||
| import kotlinx.coroutines.channels.Channel | import kotlinx.coroutines.channels.Channel | ||||||
| import kotlinx.coroutines.flow.consumeAsFlow | import kotlinx.coroutines.launch | ||||||
|  |  | ||||||
| fun <T> CoroutineScope.actor( | fun <T> CoroutineScope.actor( | ||||||
|     channelCapacity: Int = Channel.UNLIMITED, |     channelCapacity: Int = Channel.UNLIMITED, | ||||||
|     block: suspend (T) -> Unit |     block: suspend (T) -> Unit | ||||||
| ): Channel<T> { | ): Channel<T> { | ||||||
|     val channel = Channel<T>(channelCapacity) |     val channel = Channel<T>(channelCapacity) | ||||||
|     channel.consumeAsFlow().subscribe(this, block) |     launch { | ||||||
|  |         for (data in channel) { | ||||||
|  |             block(data) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|     return channel |     return channel | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,30 +0,0 @@ | |||||||
| package dev.inmo.micro_utils.coroutines |  | ||||||
|  |  | ||||||
| import kotlinx.coroutines.* |  | ||||||
| import kotlinx.coroutines.channels.Channel |  | ||||||
| import kotlinx.coroutines.flow.consumeAsFlow |  | ||||||
|  |  | ||||||
| fun <T> CoroutineScope.actorAsync( |  | ||||||
|     channelCapacity: Int = Channel.UNLIMITED, |  | ||||||
|     markerFactory: suspend (T) -> Any? = { null }, |  | ||||||
|     block: suspend (T) -> Unit |  | ||||||
| ): Channel<T> { |  | ||||||
|     val channel = Channel<T>(channelCapacity) |  | ||||||
|     channel.consumeAsFlow().subscribeAsync(this, markerFactory, block) |  | ||||||
|     return channel |  | ||||||
| } |  | ||||||
|  |  | ||||||
| inline fun <T> CoroutineScope.safeActorAsync( |  | ||||||
|     channelCapacity: Int = Channel.UNLIMITED, |  | ||||||
|     noinline onException: ExceptionHandler<Unit> = defaultSafelyExceptionHandler, |  | ||||||
|     noinline markerFactory: suspend (T) -> Any? = { null }, |  | ||||||
|     crossinline block: suspend (T) -> Unit |  | ||||||
| ): Channel<T> = actorAsync( |  | ||||||
|     channelCapacity, |  | ||||||
|     markerFactory |  | ||||||
| ) { |  | ||||||
|     safely(onException) { |  | ||||||
|         block(it) |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| @@ -6,19 +6,23 @@ import kotlin.coroutines.* | |||||||
| suspend fun <T> Iterable<Deferred<T>>.awaitFirstWithDeferred( | suspend fun <T> Iterable<Deferred<T>>.awaitFirstWithDeferred( | ||||||
|     scope: CoroutineScope, |     scope: CoroutineScope, | ||||||
|     cancelOnResult: Boolean = true |     cancelOnResult: Boolean = true | ||||||
| ): Pair<Deferred<T>, T> { | ): Pair<Deferred<T>, T> = suspendCoroutine<Pair<Deferred<T>, T>> { continuation -> | ||||||
|     val resultDeferred = CompletableDeferred<Pair<Deferred<T>, T>>() |     scope.launch(SupervisorJob()) { | ||||||
|     val scope = scope.LinkedSupervisorScope() |         val scope = this | ||||||
|         forEach { |         forEach { | ||||||
|             scope.launch { |             scope.launch { | ||||||
|             resultDeferred.complete(it to it.await()) |                 continuation.resume(it to it.await()) | ||||||
|                 scope.cancel() |                 scope.cancel() | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     return resultDeferred.await().also { |     } | ||||||
|  | }.also { | ||||||
|     if (cancelOnResult) { |     if (cancelOnResult) { | ||||||
|         forEach { |         forEach { | ||||||
|                 runCatchingSafely { it.cancel() } |             try { | ||||||
|  |                 it.cancel() | ||||||
|  |             } catch (e: IllegalStateException) { | ||||||
|  |                 e.printStackTrace() | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -1,39 +0,0 @@ | |||||||
| package dev.inmo.micro_utils.coroutines |  | ||||||
|  |  | ||||||
| import kotlinx.coroutines.flow.* |  | ||||||
| import kotlin.js.JsName |  | ||||||
| import kotlin.jvm.JvmName |  | ||||||
|  |  | ||||||
| inline fun <T, R> Flow<Flow<T>>.flatMap( |  | ||||||
|     crossinline mapper: suspend (T) -> R |  | ||||||
| ) = flow { |  | ||||||
|     collect { |  | ||||||
|         it.collect { |  | ||||||
|             emit(mapper(it)) |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| @JsName("flatMapIterable") |  | ||||||
| @JvmName("flatMapIterable") |  | ||||||
| inline fun <T, R> Flow<Iterable<T>>.flatMap( |  | ||||||
|     crossinline mapper: suspend (T) -> R |  | ||||||
| ) = map { |  | ||||||
|     it.asFlow() |  | ||||||
| }.flatMap(mapper) |  | ||||||
|  |  | ||||||
| inline fun <T, R> Flow<Flow<T>>.flatMapNotNull( |  | ||||||
|     crossinline mapper: suspend (T) -> R |  | ||||||
| ) = flatMap(mapper).takeNotNull() |  | ||||||
|  |  | ||||||
| @JsName("flatMapNotNullIterable") |  | ||||||
| @JvmName("flatMapNotNullIterable") |  | ||||||
| inline fun <T, R> Flow<Iterable<T>>.flatMapNotNull( |  | ||||||
|     crossinline mapper: suspend (T) -> R |  | ||||||
| ) = flatMap(mapper).takeNotNull() |  | ||||||
|  |  | ||||||
| fun <T> Flow<Flow<T>>.flatten() = flatMap { it } |  | ||||||
|  |  | ||||||
| @JsName("flattenIterable") |  | ||||||
| @JvmName("flattenIterable") |  | ||||||
| fun <T> Flow<Iterable<T>>.flatten() = flatMap { it } |  | ||||||
| @@ -1,6 +0,0 @@ | |||||||
| package dev.inmo.micro_utils.coroutines |  | ||||||
|  |  | ||||||
| import kotlinx.coroutines.flow.* |  | ||||||
|  |  | ||||||
| fun <T> Flow<T>.takeNotNull() = mapNotNull { it } |  | ||||||
| fun <T> Flow<T>.filterNotNull() = takeNotNull() |  | ||||||
| @@ -115,21 +115,10 @@ suspend inline fun <T> runCatchingSafely( | |||||||
|     safely(onException, block) |     safely(onException, block) | ||||||
| } | } | ||||||
|  |  | ||||||
| suspend inline fun <T, R> T.runCatchingSafely( |  | ||||||
|     noinline onException: ExceptionHandler<R> = defaultSafelyExceptionHandler, |  | ||||||
|     noinline block: suspend T.() -> R |  | ||||||
| ): Result<R> = runCatching { |  | ||||||
|     safely(onException) { block() } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| suspend inline fun <T> safelyWithResult( | suspend inline fun <T> safelyWithResult( | ||||||
|     noinline block: suspend CoroutineScope.() -> T |     noinline block: suspend CoroutineScope.() -> T | ||||||
| ): Result<T> = runCatchingSafely(defaultSafelyExceptionHandler, block) | ): Result<T> = runCatchingSafely(defaultSafelyExceptionHandler, block) | ||||||
|  |  | ||||||
| suspend inline fun <T, R> T.safelyWithResult( |  | ||||||
|     noinline block: suspend T.() -> R |  | ||||||
| ): Result<R> = runCatchingSafely(defaultSafelyExceptionHandler, block) |  | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Use this handler in cases you wish to include handling of exceptions by [defaultSafelyWithoutExceptionHandler] and |  * Use this handler in cases you wish to include handling of exceptions by [defaultSafelyWithoutExceptionHandler] and | ||||||
|  * returning null at one time |  * returning null at one time | ||||||
|   | |||||||
| @@ -1,136 +0,0 @@ | |||||||
| package dev.inmo.micro_utils.coroutines |  | ||||||
|  |  | ||||||
| import kotlinx.coroutines.currentCoroutineContext |  | ||||||
| import kotlinx.coroutines.flow.MutableStateFlow |  | ||||||
| import kotlinx.coroutines.flow.StateFlow |  | ||||||
| import kotlinx.coroutines.flow.asStateFlow |  | ||||||
| import kotlinx.coroutines.flow.first |  | ||||||
| import kotlinx.coroutines.isActive |  | ||||||
| import kotlinx.coroutines.sync.Mutex |  | ||||||
| import kotlinx.coroutines.sync.withLock |  | ||||||
| import kotlin.contracts.ExperimentalContracts |  | ||||||
| import kotlin.contracts.InvocationKind |  | ||||||
| import kotlin.contracts.contract |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * It is interface which will work like classic [Mutex], but in difference have [lockStateFlow] for listening of the |  | ||||||
|  * [SmartMutex] state. |  | ||||||
|  * |  | ||||||
|  * There is [Mutable] and [Immutable] realizations. In case you are owner and manager current state of lock, you need |  | ||||||
|  * [Mutable] [SmartMutex]. Otherwise, [Immutable]. |  | ||||||
|  * |  | ||||||
|  * Any [Mutable] [SmartMutex] may produce its [Immutable] variant which will contains [lockStateFlow] equal to its |  | ||||||
|  * [Mutable] creator |  | ||||||
|  */ |  | ||||||
| sealed interface SmartMutex { |  | ||||||
|     val lockStateFlow: StateFlow<Boolean> |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * * True - locked |  | ||||||
|      * * False - unlocked |  | ||||||
|      */ |  | ||||||
|     val isLocked: Boolean |  | ||||||
|         get() = lockStateFlow.value |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Immutable variant of [SmartMutex]. In fact will depend on the owner of [lockStateFlow] |  | ||||||
|      */ |  | ||||||
|     class Immutable(override val lockStateFlow: StateFlow<Boolean>) : SmartMutex |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Mutable variant of [SmartMutex]. With that variant you may [lock] and [unlock]. Besides, you may create |  | ||||||
|      * [Immutable] variant of [this] instance with [immutable] factory |  | ||||||
|      * |  | ||||||
|      * @param locked Preset state of [isLocked] and its internal [_lockStateFlow] |  | ||||||
|      */ |  | ||||||
|     class Mutable(locked: Boolean = false) : SmartMutex { |  | ||||||
|         private val _lockStateFlow = MutableStateFlow<Boolean>(locked) |  | ||||||
|         override val lockStateFlow: StateFlow<Boolean> = _lockStateFlow.asStateFlow() |  | ||||||
|  |  | ||||||
|         private val internalChangesMutex = Mutex() |  | ||||||
|  |  | ||||||
|         fun immutable() = Immutable(lockStateFlow) |  | ||||||
|  |  | ||||||
|         /** |  | ||||||
|          * Holds call until this [SmartMutex] will be re-locked. That means that while [isLocked] == true, [holds] will |  | ||||||
|          * wait for [isLocked] == false and then try to lock |  | ||||||
|          */ |  | ||||||
|         suspend fun lock() { |  | ||||||
|             do { |  | ||||||
|                 waitUnlock() |  | ||||||
|                 val shouldContinue = internalChangesMutex.withLock { |  | ||||||
|                     if (_lockStateFlow.value) { |  | ||||||
|                         true |  | ||||||
|                     } else { |  | ||||||
|                         _lockStateFlow.value = true |  | ||||||
|                         false |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|             } while (shouldContinue && currentCoroutineContext().isActive) |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         /** |  | ||||||
|          * Will try to lock this [SmartMutex] immediataly |  | ||||||
|          * |  | ||||||
|          * @return True if lock was successful. False otherwise |  | ||||||
|          */ |  | ||||||
|         suspend fun tryLock(): Boolean { |  | ||||||
|             return if (!_lockStateFlow.value) { |  | ||||||
|                 internalChangesMutex.withLock { |  | ||||||
|                     if (!_lockStateFlow.value) { |  | ||||||
|                         _lockStateFlow.value = true |  | ||||||
|                         true |  | ||||||
|                     } else { |  | ||||||
|                         false |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|             } else { |  | ||||||
|                 false |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         /** |  | ||||||
|          * If [isLocked] == true - will change it to false and return true. If current call will not unlock this |  | ||||||
|          * [SmartMutex] - false |  | ||||||
|          */ |  | ||||||
|         suspend fun unlock(): Boolean { |  | ||||||
|             return if (_lockStateFlow.value) { |  | ||||||
|                 internalChangesMutex.withLock { |  | ||||||
|                     if (_lockStateFlow.value) { |  | ||||||
|                         _lockStateFlow.value = false |  | ||||||
|                         true |  | ||||||
|                     } else { |  | ||||||
|                         false |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|             } else { |  | ||||||
|                 false |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Will call [SmartMutex.Mutable.lock], then execute [action] and return the result after [SmartMutex.Mutable.unlock] |  | ||||||
|  */ |  | ||||||
| @OptIn(ExperimentalContracts::class) |  | ||||||
| suspend inline fun <T> SmartMutex.Mutable.withLock(action: () -> T): T { |  | ||||||
|     contract { |  | ||||||
|         callsInPlace(action, InvocationKind.EXACTLY_ONCE) |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     lock() |  | ||||||
|     try { |  | ||||||
|         return action() |  | ||||||
|     } finally { |  | ||||||
|         unlock() |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Will wait until the [SmartMutex.lockStateFlow] of [this] instance will be false. |  | ||||||
|  * |  | ||||||
|  * Anyway, after the end of this block there are no any guaranties that [SmartMutex.isLocked] == false due to the fact |  | ||||||
|  * that some other parties may lock it again |  | ||||||
|  */ |  | ||||||
| suspend fun SmartMutex.waitUnlock() = lockStateFlow.first { !it } |  | ||||||
| @@ -1,105 +0,0 @@ | |||||||
| package dev.inmo.micro_utils.coroutines |  | ||||||
|  |  | ||||||
| import kotlin.contracts.ExperimentalContracts |  | ||||||
| import kotlin.contracts.InvocationKind |  | ||||||
| import kotlin.contracts.contract |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Composite mutex which works with next rules: |  | ||||||
|  * |  | ||||||
|  * * [acquireRead] require to [writeMutex] be free. Then it will take one lock from [readSemaphore] |  | ||||||
|  * * [releaseRead] will just free up one permit in [readSemaphore] |  | ||||||
|  * * [lockWrite] will lock [writeMutex] and then await while all [readSemaphore] will be freed |  | ||||||
|  * * [unlockWrite] will just unlock [writeMutex] |  | ||||||
|  */ |  | ||||||
| class SmartRWLocker(private val readPermits: Int = Int.MAX_VALUE, writeIsLocked: Boolean = false) { |  | ||||||
|     private val _readSemaphore = SmartSemaphore.Mutable(permits = readPermits, acquiredPermits = 0) |  | ||||||
|     private val _writeMutex = SmartMutex.Mutable(locked = writeIsLocked) |  | ||||||
|  |  | ||||||
|     val readSemaphore: SmartSemaphore.Immutable = _readSemaphore.immutable() |  | ||||||
|     val writeMutex: SmartMutex.Immutable = _writeMutex.immutable() |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Do lock in [readSemaphore] inside of [writeMutex] locking |  | ||||||
|      */ |  | ||||||
|     suspend fun acquireRead() { |  | ||||||
|         _writeMutex.waitUnlock() |  | ||||||
|         _readSemaphore.acquire() |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Release one read permit in [readSemaphore] |  | ||||||
|      */ |  | ||||||
|     suspend fun releaseRead(): Boolean { |  | ||||||
|         return _readSemaphore.release() |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Locking [writeMutex] and wait while all [readSemaphore] permits will be freed |  | ||||||
|      */ |  | ||||||
|     suspend fun lockWrite() { |  | ||||||
|         _writeMutex.lock() |  | ||||||
|         _readSemaphore.acquire(readPermits) |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Unlock [writeMutex] |  | ||||||
|      */ |  | ||||||
|     suspend fun unlockWrite(): Boolean { |  | ||||||
|         return _writeMutex.unlock().also { |  | ||||||
|             if (it) { |  | ||||||
|                 _readSemaphore.release(readPermits) |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Will call [SmartSemaphore.Mutable.lock], then execute [action] and return the result after [SmartSemaphore.Mutable.unlock] |  | ||||||
|  */ |  | ||||||
| @OptIn(ExperimentalContracts::class) |  | ||||||
| suspend inline fun <T> SmartRWLocker.withReadAcquire(action: () -> T): T { |  | ||||||
|     contract { |  | ||||||
|         callsInPlace(action, InvocationKind.EXACTLY_ONCE) |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     acquireRead() |  | ||||||
|     try { |  | ||||||
|         return action() |  | ||||||
|     } finally { |  | ||||||
|         releaseRead() |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Will wait until the [SmartSemaphore.permitsStateFlow] of [this] instance will have [permits] count free permits. |  | ||||||
|  * |  | ||||||
|  * Anyway, after the end of this block there are no any guaranties that [SmartSemaphore.freePermits] >= [permits] due to |  | ||||||
|  * the fact that some other parties may lock it again |  | ||||||
|  */ |  | ||||||
| suspend fun SmartRWLocker.waitReadRelease(permits: Int = 1) = readSemaphore.waitRelease(permits) |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Will call [SmartMutex.Mutable.lock], then execute [action] and return the result after [SmartMutex.Mutable.unlock] |  | ||||||
|  */ |  | ||||||
| @OptIn(ExperimentalContracts::class) |  | ||||||
| suspend inline fun <T> SmartRWLocker.withWriteLock(action: () -> T): T { |  | ||||||
|     contract { |  | ||||||
|         callsInPlace(action, InvocationKind.EXACTLY_ONCE) |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     lockWrite() |  | ||||||
|     try { |  | ||||||
|         return action() |  | ||||||
|     } finally { |  | ||||||
|         unlockWrite() |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Will wait until the [SmartMutex.lockStateFlow] of [this] instance will be false. |  | ||||||
|  * |  | ||||||
|  * Anyway, after the end of this block there are no any guaranties that [SmartMutex.isLocked] == false due to the fact |  | ||||||
|  * that some other parties may lock it again |  | ||||||
|  */ |  | ||||||
| suspend fun SmartRWLocker.waitWriteUnlock() = writeMutex.waitUnlock() |  | ||||||
| @@ -1,168 +0,0 @@ | |||||||
| package dev.inmo.micro_utils.coroutines |  | ||||||
|  |  | ||||||
| import kotlinx.coroutines.currentCoroutineContext |  | ||||||
| import kotlinx.coroutines.flow.MutableStateFlow |  | ||||||
| import kotlinx.coroutines.flow.StateFlow |  | ||||||
| import kotlinx.coroutines.flow.asStateFlow |  | ||||||
| import kotlinx.coroutines.flow.first |  | ||||||
| import kotlinx.coroutines.isActive |  | ||||||
| import kotlinx.coroutines.sync.Mutex |  | ||||||
| import kotlinx.coroutines.sync.Semaphore |  | ||||||
| import kotlinx.coroutines.sync.withLock |  | ||||||
| import kotlin.contracts.ExperimentalContracts |  | ||||||
| import kotlin.contracts.InvocationKind |  | ||||||
| import kotlin.contracts.contract |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * It is interface which will work like classic [Semaphore], but in difference have [permitsStateFlow] for listening of the |  | ||||||
|  * [SmartSemaphore] state. |  | ||||||
|  * |  | ||||||
|  * There is [Mutable] and [Immutable] realizations. In case you are owner and manager current state of lock, you need |  | ||||||
|  * [Mutable] [SmartSemaphore]. Otherwise, [Immutable]. |  | ||||||
|  * |  | ||||||
|  * Any [Mutable] [SmartSemaphore] may produce its [Immutable] variant which will contains [permitsStateFlow] equal to its |  | ||||||
|  * [Mutable] creator |  | ||||||
|  */ |  | ||||||
| sealed interface SmartSemaphore { |  | ||||||
|     val permitsStateFlow: StateFlow<Int> |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * * True - locked |  | ||||||
|      * * False - unlocked |  | ||||||
|      */ |  | ||||||
|     val freePermits: Int |  | ||||||
|         get() = permitsStateFlow.value |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Immutable variant of [SmartSemaphore]. In fact will depend on the owner of [permitsStateFlow] |  | ||||||
|      */ |  | ||||||
|     class Immutable(override val permitsStateFlow: StateFlow<Int>) : SmartSemaphore |  | ||||||
|  |  | ||||||
|     /** |  | ||||||
|      * Mutable variant of [SmartSemaphore]. With that variant you may [lock] and [unlock]. Besides, you may create |  | ||||||
|      * [Immutable] variant of [this] instance with [immutable] factory |  | ||||||
|      * |  | ||||||
|      * @param locked Preset state of [freePermits] and its internal [_freePermitsStateFlow] |  | ||||||
|      */ |  | ||||||
|     class Mutable(private val permits: Int, acquiredPermits: Int = 0) : SmartSemaphore { |  | ||||||
|         private val _freePermitsStateFlow = MutableStateFlow<Int>(permits - acquiredPermits) |  | ||||||
|         override val permitsStateFlow: StateFlow<Int> = _freePermitsStateFlow.asStateFlow() |  | ||||||
|  |  | ||||||
|         private val internalChangesMutex = Mutex(false) |  | ||||||
|  |  | ||||||
|         fun immutable() = Immutable(permitsStateFlow) |  | ||||||
|  |  | ||||||
|         private fun checkedPermits(permits: Int) = permits.coerceIn(1 .. this.permits) |  | ||||||
|  |  | ||||||
|         /** |  | ||||||
|          * Holds call until this [SmartSemaphore] will be re-locked. That means that current method will |  | ||||||
|          */ |  | ||||||
|         suspend fun acquire(permits: Int = 1) { |  | ||||||
|             var acquiredPermits = 0 |  | ||||||
|             val checkedPermits = checkedPermits(permits) |  | ||||||
|             try { |  | ||||||
|                 do { |  | ||||||
|                     val shouldContinue = internalChangesMutex.withLock { |  | ||||||
|                         val requiredPermits = checkedPermits - acquiredPermits |  | ||||||
|                         val acquiring = minOf(freePermits, requiredPermits).takeIf { it > 0 } ?: return@withLock true |  | ||||||
|                         acquiredPermits += acquiring |  | ||||||
|                         _freePermitsStateFlow.value -= acquiring |  | ||||||
|  |  | ||||||
|                         acquiredPermits != checkedPermits |  | ||||||
|                     } |  | ||||||
|                     if (shouldContinue) { |  | ||||||
|                         waitRelease() |  | ||||||
|                     } |  | ||||||
|                 } while (shouldContinue && currentCoroutineContext().isActive) |  | ||||||
|             } catch (e: Throwable) { |  | ||||||
|                 release(acquiredPermits) |  | ||||||
|                 throw e |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         /** |  | ||||||
|          * Holds call until this [SmartSemaphore] will be re-locked. That means that while [freePermits] == true, [holds] will |  | ||||||
|          * wait for [freePermits] == false and then try to lock |  | ||||||
|          */ |  | ||||||
|         suspend fun acquireByOne(permits: Int = 1) { |  | ||||||
|             val checkedPermits = checkedPermits(permits) |  | ||||||
|             do { |  | ||||||
|                 waitRelease(checkedPermits) |  | ||||||
|                 val shouldContinue = internalChangesMutex.withLock { |  | ||||||
|                     if (_freePermitsStateFlow.value < checkedPermits) { |  | ||||||
|                         true |  | ||||||
|                     } else { |  | ||||||
|                         _freePermitsStateFlow.value -= checkedPermits |  | ||||||
|                         false |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|             } while (shouldContinue && currentCoroutineContext().isActive) |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         /** |  | ||||||
|          * Will try to lock this [SmartSemaphore] immediataly |  | ||||||
|          * |  | ||||||
|          * @return True if lock was successful. False otherwise |  | ||||||
|          */ |  | ||||||
|         suspend fun tryAcquire(permits: Int = 1): Boolean { |  | ||||||
|             val checkedPermits = checkedPermits(permits) |  | ||||||
|             return if (_freePermitsStateFlow.value < checkedPermits) { |  | ||||||
|                 internalChangesMutex.withLock { |  | ||||||
|                     if (_freePermitsStateFlow.value < checkedPermits) { |  | ||||||
|                         _freePermitsStateFlow.value -= checkedPermits |  | ||||||
|                         true |  | ||||||
|                     } else { |  | ||||||
|                         false |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|             } else { |  | ||||||
|                 false |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         /** |  | ||||||
|          * If [freePermits] == true - will change it to false and return true. If current call will not unlock this |  | ||||||
|          * [SmartSemaphore] - false |  | ||||||
|          */ |  | ||||||
|         suspend fun release(permits: Int = 1): Boolean { |  | ||||||
|             val checkedPermits = checkedPermits(permits) |  | ||||||
|             return if (_freePermitsStateFlow.value < this.permits) { |  | ||||||
|                 internalChangesMutex.withLock { |  | ||||||
|                     if (_freePermitsStateFlow.value < this.permits) { |  | ||||||
|                         _freePermitsStateFlow.value = minOf(_freePermitsStateFlow.value + checkedPermits, this.permits) |  | ||||||
|                         true |  | ||||||
|                     } else { |  | ||||||
|                         false |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|             } else { |  | ||||||
|                 false |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Will call [SmartSemaphore.Mutable.lock], then execute [action] and return the result after [SmartSemaphore.Mutable.unlock] |  | ||||||
|  */ |  | ||||||
| @OptIn(ExperimentalContracts::class) |  | ||||||
| suspend inline fun <T> SmartSemaphore.Mutable.withAcquire(permits: Int = 1, action: () -> T): T { |  | ||||||
|     contract { |  | ||||||
|         callsInPlace(action, InvocationKind.EXACTLY_ONCE) |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     acquire(permits) |  | ||||||
|     try { |  | ||||||
|         return action() |  | ||||||
|     } finally { |  | ||||||
|         release(permits) |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Will wait until the [SmartSemaphore.permitsStateFlow] of [this] instance will have [permits] count free permits. |  | ||||||
|  * |  | ||||||
|  * Anyway, after the end of this block there are no any guaranties that [SmartSemaphore.freePermits] >= [permits] due to |  | ||||||
|  * the fact that some other parties may lock it again |  | ||||||
|  */ |  | ||||||
| suspend fun SmartSemaphore.waitRelease(permits: Int = 1) = permitsStateFlow.first { it >= permits } |  | ||||||
| @@ -1,86 +0,0 @@ | |||||||
| package dev.inmo.micro_utils.coroutines |  | ||||||
|  |  | ||||||
| import kotlinx.coroutines.CoroutineScope |  | ||||||
| import kotlinx.coroutines.Dispatchers |  | ||||||
| import kotlinx.coroutines.ExperimentalCoroutinesApi |  | ||||||
| import kotlinx.coroutines.InternalCoroutinesApi |  | ||||||
| import kotlinx.coroutines.channels.BufferOverflow |  | ||||||
| import kotlinx.coroutines.flow.FlowCollector |  | ||||||
| import kotlinx.coroutines.flow.MutableSharedFlow |  | ||||||
| import kotlinx.coroutines.flow.MutableStateFlow |  | ||||||
| import kotlinx.coroutines.flow.StateFlow |  | ||||||
| import kotlinx.coroutines.internal.SynchronizedObject |  | ||||||
| import kotlinx.coroutines.internal.synchronized |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * Works like [StateFlow], but guarantee that latest value update will always be delivered to |  | ||||||
|  * each active subscriber |  | ||||||
|  */ |  | ||||||
| open class SpecialMutableStateFlow<T>( |  | ||||||
|     initialValue: T, |  | ||||||
|     internalScope: CoroutineScope = CoroutineScope(Dispatchers.Default) |  | ||||||
| ) : MutableStateFlow<T>, FlowCollector<T>, MutableSharedFlow<T> { |  | ||||||
|     @OptIn(InternalCoroutinesApi::class) |  | ||||||
|     private val syncObject = SynchronizedObject() |  | ||||||
|     protected val internalSharedFlow: MutableSharedFlow<T> = MutableSharedFlow( |  | ||||||
|         replay = 0, |  | ||||||
|         extraBufferCapacity = 2, |  | ||||||
|         onBufferOverflow = BufferOverflow.DROP_OLDEST |  | ||||||
|     ) |  | ||||||
|     protected val publicSharedFlow: MutableSharedFlow<T> = MutableSharedFlow( |  | ||||||
|         replay = 1, |  | ||||||
|         extraBufferCapacity = 1, |  | ||||||
|         onBufferOverflow = BufferOverflow.DROP_OLDEST |  | ||||||
|     ) |  | ||||||
|  |  | ||||||
|     protected var _value: T = initialValue |  | ||||||
|     override var value: T |  | ||||||
|         get() = _value |  | ||||||
|         set(value) { |  | ||||||
|             doOnChangeAction(value) |  | ||||||
|         } |  | ||||||
|     protected val job = internalSharedFlow.subscribe(internalScope) { |  | ||||||
|         doOnChangeAction(it) |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     override val replayCache: List<T> |  | ||||||
|         get() = publicSharedFlow.replayCache |  | ||||||
|     override val subscriptionCount: StateFlow<Int> |  | ||||||
|         get() = publicSharedFlow.subscriptionCount |  | ||||||
|  |  | ||||||
|     @OptIn(InternalCoroutinesApi::class) |  | ||||||
|     override fun compareAndSet(expect: T, update: T): Boolean { |  | ||||||
|         return synchronized(syncObject) { |  | ||||||
|             if (expect == _value && update != _value) { |  | ||||||
|                 doOnChangeAction(update) |  | ||||||
|             } |  | ||||||
|             expect == _value |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     protected open fun onChangeWithoutSync(value: T) { |  | ||||||
|         _value = value |  | ||||||
|         publicSharedFlow.tryEmit(value) |  | ||||||
|     } |  | ||||||
|     @OptIn(InternalCoroutinesApi::class) |  | ||||||
|     protected open fun doOnChangeAction(value: T) { |  | ||||||
|         synchronized(syncObject) { |  | ||||||
|             if (_value != value) { |  | ||||||
|                 onChangeWithoutSync(value) |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @ExperimentalCoroutinesApi |  | ||||||
|     override fun resetReplayCache() = publicSharedFlow.resetReplayCache() |  | ||||||
|  |  | ||||||
|     override fun tryEmit(value: T): Boolean { |  | ||||||
|         return internalSharedFlow.tryEmit(value) |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     override suspend fun emit(value: T) { |  | ||||||
|         internalSharedFlow.emit(value) |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     override suspend fun collect(collector: FlowCollector<T>) = publicSharedFlow.collect(collector) |  | ||||||
| } |  | ||||||
| @@ -1,151 +0,0 @@ | |||||||
| import dev.inmo.micro_utils.coroutines.* |  | ||||||
| import kotlinx.coroutines.* |  | ||||||
| import kotlinx.coroutines.flow.first |  | ||||||
| import kotlinx.coroutines.sync.Mutex |  | ||||||
| import kotlinx.coroutines.sync.withLock |  | ||||||
| import kotlinx.coroutines.test.runTest |  | ||||||
| import kotlin.test.Test |  | ||||||
| import kotlin.test.assertEquals |  | ||||||
| import kotlin.test.assertTrue |  | ||||||
|  |  | ||||||
| class SmartRWLockerTests { |  | ||||||
|     @Test |  | ||||||
|     fun compositeTest() { |  | ||||||
|         val locker = SmartRWLocker() |  | ||||||
|  |  | ||||||
|         val readAndWriteWorkers = 10 |  | ||||||
|         runTest { |  | ||||||
|             var started = 0 |  | ||||||
|             var done = 0 |  | ||||||
|             val doneMutex = Mutex() |  | ||||||
|             val readWorkers = (0 until readAndWriteWorkers).map { |  | ||||||
|                 launch(start = CoroutineStart.LAZY) { |  | ||||||
|                     locker.withReadAcquire { |  | ||||||
|                         doneMutex.withLock { |  | ||||||
|                             started++ |  | ||||||
|                         } |  | ||||||
|                         delay(100L) |  | ||||||
|                         doneMutex.withLock { |  | ||||||
|                             done++ |  | ||||||
|                         } |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             var doneWrites = 0 |  | ||||||
|  |  | ||||||
|             val writeWorkers = (0 until readAndWriteWorkers).map { |  | ||||||
|                 launch(start = CoroutineStart.LAZY) { |  | ||||||
|                     locker.withWriteLock { |  | ||||||
|                         assertTrue(done == readAndWriteWorkers || started == 0) |  | ||||||
|                         delay(10L) |  | ||||||
|                         doneWrites++ |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|             readWorkers.forEach { it.start() } |  | ||||||
|             writeWorkers.forEach { it.start() } |  | ||||||
|  |  | ||||||
|             readWorkers.joinAll() |  | ||||||
|             writeWorkers.joinAll() |  | ||||||
|  |  | ||||||
|             assertEquals(expected = readAndWriteWorkers, actual = done) |  | ||||||
|             assertEquals(expected = readAndWriteWorkers, actual = doneWrites) |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Test |  | ||||||
|     fun simpleWithWriteLockTest() { |  | ||||||
|         val locker = SmartRWLocker() |  | ||||||
|  |  | ||||||
|         runTest { |  | ||||||
|             locker.withWriteLock { |  | ||||||
|                 assertEquals(0, locker.readSemaphore.freePermits) |  | ||||||
|                 assertEquals(true, locker.writeMutex.isLocked) |  | ||||||
|             } |  | ||||||
|             assertEquals(Int.MAX_VALUE, locker.readSemaphore.freePermits) |  | ||||||
|             assertEquals(false, locker.writeMutex.isLocked) |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Test |  | ||||||
|     fun failureWithWriteLockTest() { |  | ||||||
|         val locker = SmartRWLocker() |  | ||||||
|  |  | ||||||
|         val exception = IllegalArgumentException() |  | ||||||
|         try { |  | ||||||
|             runTest { |  | ||||||
|                 val subscope = kotlinx.coroutines.CoroutineScope(this.coroutineContext) |  | ||||||
|                 var happenException: Throwable? = null |  | ||||||
|                 try { |  | ||||||
|                     locker.withWriteLock { |  | ||||||
|                         val checkFunction = fun (): Deferred<Unit> { |  | ||||||
|                             return subscope.async { |  | ||||||
|                                 assertEquals(0, locker.readSemaphore.freePermits) |  | ||||||
|                                 assertEquals(true, locker.writeMutex.isLocked) |  | ||||||
|                                 throw exception |  | ||||||
|                             } |  | ||||||
|                         } |  | ||||||
|                         doInDefault { |  | ||||||
|                             assertEquals(0, locker.readSemaphore.freePermits) |  | ||||||
|                             assertEquals(true, locker.writeMutex.isLocked) |  | ||||||
|                             checkFunction().await() |  | ||||||
|                         } |  | ||||||
|                     } |  | ||||||
|                 } catch (e: Exception) { |  | ||||||
|                     happenException = e |  | ||||||
|                 } |  | ||||||
|                 if (exception != happenException) { |  | ||||||
|                     assertEquals(exception, happenException ?.cause) |  | ||||||
|                 } |  | ||||||
|                 assertEquals(Int.MAX_VALUE, locker.readSemaphore.freePermits) |  | ||||||
|                 assertEquals(false, locker.writeMutex.isLocked) |  | ||||||
|             } |  | ||||||
|         } catch (e: Exception) { |  | ||||||
|             assertEquals(exception, e) |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Test |  | ||||||
|     fun simpleWithReadAcquireTest() { |  | ||||||
|         val locker = SmartRWLocker() |  | ||||||
|  |  | ||||||
|         runTest { |  | ||||||
|             locker.withReadAcquire { |  | ||||||
|                 assertEquals(Int.MAX_VALUE - 1, locker.readSemaphore.freePermits) |  | ||||||
|                 assertEquals(false, locker.writeMutex.isLocked) |  | ||||||
|                 locker.withReadAcquire { |  | ||||||
|                     assertEquals(Int.MAX_VALUE - 2, locker.readSemaphore.freePermits) |  | ||||||
|                     assertEquals(false, locker.writeMutex.isLocked) |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|             assertEquals(Int.MAX_VALUE, locker.readSemaphore.freePermits) |  | ||||||
|             assertEquals(false, locker.writeMutex.isLocked) |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     @Test |  | ||||||
|     fun simple2WithWriteLockTest() { |  | ||||||
|         val locker = SmartRWLocker() |  | ||||||
|  |  | ||||||
|         val unlockDelay = 1000L // 1 sec |  | ||||||
|         var unlocked: Boolean = false |  | ||||||
|         runTest { |  | ||||||
|             launch { |  | ||||||
|                 locker.withReadAcquire { |  | ||||||
|                     delay(unlockDelay) |  | ||||||
|                 } |  | ||||||
|                 unlocked = true |  | ||||||
|             } |  | ||||||
|             locker.readSemaphore.permitsStateFlow.first { it == Int.MAX_VALUE - 1 } |  | ||||||
|             assertEquals(false, unlocked) |  | ||||||
|             locker.withWriteLock { |  | ||||||
|                 assertEquals(true, unlocked) |  | ||||||
|                 assertEquals(0, locker.readSemaphore.freePermits) |  | ||||||
|                 assertEquals(true, locker.writeMutex.isLocked) |  | ||||||
|             } |  | ||||||
|             assertEquals(Int.MAX_VALUE, locker.readSemaphore.freePermits) |  | ||||||
|             assertEquals(false, locker.writeMutex.isLocked) |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,26 +0,0 @@ | |||||||
| package dev.inmo.micro_utils.coroutines |  | ||||||
|  |  | ||||||
| import kotlinx.coroutines.CancellationException |  | ||||||
| import kotlinx.coroutines.Job |  | ||||||
| import org.w3c.dom.Image |  | ||||||
|  |  | ||||||
| suspend fun preloadImage(src: String): Image { |  | ||||||
|     val image = Image() |  | ||||||
|     image.src = src |  | ||||||
|  |  | ||||||
|     val job = Job() |  | ||||||
|  |  | ||||||
|     image.addEventListener("load", { |  | ||||||
|         runCatching { job.complete() } |  | ||||||
|     }) |  | ||||||
|  |  | ||||||
|     runCatchingSafely { |  | ||||||
|         job.join() |  | ||||||
|     }.onFailure { |  | ||||||
|         if (it is CancellationException) { |  | ||||||
|             image.src = "" |  | ||||||
|         } |  | ||||||
|     }.getOrThrow() |  | ||||||
|  |  | ||||||
|     return image |  | ||||||
| } |  | ||||||
| @@ -1,9 +0,0 @@ | |||||||
| package dev.inmo.micro_utils.coroutines |  | ||||||
|  |  | ||||||
| import kotlinx.coroutines.CoroutineScope |  | ||||||
| import kotlinx.coroutines.Dispatchers |  | ||||||
|  |  | ||||||
| fun <T> launchInCurrentThread(block: suspend CoroutineScope.() -> T): T { |  | ||||||
|     val scope = CoroutineScope(Dispatchers.Unconfined) |  | ||||||
|     return scope.launchSynchronously(block) |  | ||||||
| } |  | ||||||
| @@ -6,7 +6,7 @@ fun <T> CoroutineScope.launchSynchronously(block: suspend CoroutineScope.() -> T | |||||||
|     var result: Result<T>? = null |     var result: Result<T>? = null | ||||||
|     val objectToSynchronize = Object() |     val objectToSynchronize = Object() | ||||||
|     synchronized(objectToSynchronize) { |     synchronized(objectToSynchronize) { | ||||||
|         launch(start = CoroutineStart.UNDISPATCHED) { |         launch { | ||||||
|             result = safelyWithResult(block) |             result = safelyWithResult(block) | ||||||
|         }.invokeOnCompletion { |         }.invokeOnCompletion { | ||||||
|             synchronized(objectToSynchronize) { |             synchronized(objectToSynchronize) { | ||||||
|   | |||||||
| @@ -1,47 +0,0 @@ | |||||||
| package dev.inmo.micro_utils.coroutines |  | ||||||
|  |  | ||||||
| import kotlinx.coroutines.Dispatchers |  | ||||||
| import kotlinx.coroutines.delay |  | ||||||
| import kotlinx.coroutines.withContext |  | ||||||
| import kotlin.test.Test |  | ||||||
| import kotlin.test.assertEquals |  | ||||||
|  |  | ||||||
| class LaunchInCurrentThreadTests { |  | ||||||
|     @Test |  | ||||||
|     fun simpleTestThatLaunchInCurrentThreadWorks() { |  | ||||||
|         val expectedResult = 10 |  | ||||||
|         val result = launchInCurrentThread { |  | ||||||
|             expectedResult |  | ||||||
|         } |  | ||||||
|         assertEquals(expectedResult, result) |  | ||||||
|     } |  | ||||||
|     @Test |  | ||||||
|     fun simpleTestThatSeveralLaunchInCurrentThreadWorks() { |  | ||||||
|         val testData = 0 until 100 |  | ||||||
|  |  | ||||||
|         testData.forEach { |  | ||||||
|             val result = launchInCurrentThread { |  | ||||||
|                 it |  | ||||||
|             } |  | ||||||
|             assertEquals(it, result) |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|     @Test |  | ||||||
|     fun simpleTestThatLaunchInCurrentThreadWillCorrectlyHandleSuspensionsWorks() { |  | ||||||
|         val testData = 0 until 100 |  | ||||||
|  |  | ||||||
|         suspend fun test(data: Any): Any { |  | ||||||
|             return withContext(Dispatchers.Default) { |  | ||||||
|                 delay(1) |  | ||||||
|                 data |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         testData.forEach { |  | ||||||
|             val result = launchInCurrentThread { |  | ||||||
|                 test(it) |  | ||||||
|             } |  | ||||||
|             assertEquals(it, result) |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
							
								
								
									
										1
									
								
								coroutines/src/main/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								coroutines/src/main/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | <manifest package="dev.inmo.micro_utils.coroutines"/> | ||||||
| @@ -4,14 +4,13 @@ plugins { | |||||||
|     id "com.android.library" |     id "com.android.library" | ||||||
| } | } | ||||||
|  |  | ||||||
| apply from: "$mppJvmJsAndroidLinuxMingwLinuxArm64ProjectPresetPath" | apply from: "$mppProjectWithSerializationPresetPath" | ||||||
|  |  | ||||||
| kotlin { | kotlin { | ||||||
|     sourceSets { |     sourceSets { | ||||||
|         commonMain { |         commonMain { | ||||||
|             dependencies { |             dependencies { | ||||||
|                 api project(":micro_utils.common") |                 api project(":micro_utils.common") | ||||||
|                 api libs.krypto |  | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         jsMain { |         jsMain { | ||||||
|   | |||||||
| @@ -1 +0,0 @@ | |||||||
| <manifest/> |  | ||||||
| @@ -1,15 +0,0 @@ | |||||||
| package dev.inmo.micro_utils.crypto |  | ||||||
|  |  | ||||||
| val HEX_ARRAY = "0123456789abcdef".toCharArray() |  | ||||||
|  |  | ||||||
| fun SourceBytes.hex(): String { |  | ||||||
|     val hexChars = CharArray(size * 2) |  | ||||||
|     for (j in indices) { |  | ||||||
|         val v: Int = this[j].toInt() and 0xFF |  | ||||||
|         hexChars[j * 2] = HEX_ARRAY[v ushr 4] |  | ||||||
|         hexChars[j * 2 + 1] = HEX_ARRAY[v and 0x0F] |  | ||||||
|     } |  | ||||||
|     return hexChars.concatToString() |  | ||||||
| } |  | ||||||
|  |  | ||||||
| fun SourceString.hex(): String = encodeToByteArray().hex() |  | ||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user